MRが楽しい

MRやVRについて学習したことを書き残す

C#でDeepCopyを利用する

本日は C# の小ネタ枠です。
C# で DeepCopy を利用する方法です。

シャロウコピーとディープコピー

変数のコピーには、シャロウコピーとディープコピーの2種類のコピーがあります。
C# で等価演算子を使って変数をコピーした場合、参照型の変数はシャロウコピーが行われ、実体はコピーされずに参照の設定のみがコピーされます
コピー先の変数とコピー元の変数で同じ実体を参照することになるため、どちらかの変数の実体を操作すると双方に影響があります。

例えば以下のような処理でシャロウコピーが行われます。

        // 参照型の変数を作成する
        Member member01 = new Member { Name = "alpha", Address = "01" };
        // 変数をシャロウコピーする
        var shallowCopy = member01;
        // コピー先の変数から実体を操作する
        shallowCopy.Address = "02";

        // コピー元とコピー先の両方が影響を受ける
        Debug.Log($"member01 : Name = {member01.Name}, Address = {member01.Address} ;");
        Debug.Log($"shallowCopy : Name = {shallowCopy.Name}, Address = {shallowCopy.Address} ;");

一方で実体もコピーして別々の変数として扱えるのがディープコピーです。
C# ではディープコピーの標準関数がないため、ディープコピーを行いたい場合は関数を自作する必要があります。
後述の参考サイトを基に、以下のディープコピー用関数を作成しました。
・CopyHelper.cs

using System.IO;
using System.Runtime.Serialization.Formatters.Binary;

/// <summary>
/// コピー用汎用関数
/// </summary>
public static class CopyHelper
{
    /// <summary>
    /// 対象のディープコピーを行う
    /// シリアライズ(Serializable 属性)されていないクラスではエラー
    /// </summary>
    /// <typeparam name="T"></typeparam>
    /// <param name="src"></param>
    /// <returns></returns>
    public static T DeepCopy<T>(this T src)
    {
        using (MemoryStream stream = new MemoryStream())
        {
            var formatter = new BinaryFormatter();
            formatter.Serialize(stream, src);
            stream.Position = 0;

            return (T)formatter.Deserialize(stream);
        }
    }
}

以下のように利用してディープコピーを実行できます。

        // 参照型の変数を作成する
        Member member01 = new Member { Name = "alpha", Address = "01" };
        // 自作した DeepCopy 関数を呼び出して変数をディープコピーする
        var deepCopy = member01.DeepCopy();
        // コピー先の変数で実体を操作する
        deepCopy.Address = "02";

        // コピー元の実体はコピー先と異なるので影響を受けない
        Debug.Log($"member01 : Name = {member01.Name}, Address = {member01.Address} ;");
        Debug.Log($"deepCopy : Name = {deepCopy.Name}, Address = {deepCopy.Address} ;");


ディープコピーを行う際の注意点

ディープコピーを行う対象はシリアライズ可能なクラスである必要があります。
自作クラスをディープコピーする場合は、クラスに Serializable 属性を設定します。
・Member.cs

using System;

/// <summary>
/// メンバークラス
/// </summary>
[Serializable] // DeepCopy対象では Serializable 属性が必須
public class Member
{
    public string Name { get; set; }

    public string Address { get; set; }
}

Unity での注意点

MonoBehaviour を継承したコンポーネントシリアライズできないため、DeepCopy を実行すると SerializationException が発生します。

        // MonoBehaviour を継承したコンポーネントを取得する
        MemberMonoBehaviour member01 = this.GetComponent<MemberMonoBehaviour>();
        // 自作した DeepCopy 関数を呼び出して変数をディープコピーする
        // MonoBehaviour を継承したコンポーネントの場合、ここで SerializationException エラーが発生する
        var deepCopy = member01.DeepCopy();
        // コピー先の変数で実体を操作する
        deepCopy.Address = "02";

        Debug.Log($"member01 : Name = {member01.Name}, Address = {member01.Address} ;");
        Debug.Log($"deepCopy : Name = {deepCopy.Name}, Address = {deepCopy.Address} ;");

・MemberMonoBehaviour.cs

using System;
using UnityEngine;

[Serializable]
public class MemberMonoBehaviour : MonoBehaviour
{
    public string Name { get; set; }

    public string Address { get; set; }
}


その他のディープコピー

Listの場合

Listに対するディープコピーを試しました。

        Member member01 = new Member { Name = "alpha", Address = "01" };
        Member member02 = new Member { Name = "beta", Address = "11" };
        List<Member> members = new List<Member> { member01, member02 };
        var deepCopy = members.DeepCopy();
        deepCopy[0].Address = "02";
        foreach (var member in members)
        {
            Debug.Log($"member : Name = {member.Name}, Address = {member.Address} ;");
        }
        foreach (var member in deepCopy)
        {
            Debug.Log($"deepCopy : Name = {member.Name}, Address = {member.Address} ;");
        }


自作クラスを含む場合

自作クラスを含む自作クラスに対するディープコピーを試しました。

        Member member01 = new Member { Name = "alpha", Address = "01" };
        Member member02 = new Member { Name = "beta", Address = "11" };
        List<Member> members = new List<Member> { member01, member02 };
        Teams teams = new Teams { TeamName = "abc", Members = members };
        var deepCopy = teams.DeepCopy();
        deepCopy.TeamName = "123";
        deepCopy.Members[1].Address = "12";

        Debug.Log($"teams : TeamName = {teams.TeamName} ;");
        foreach (var member in teams.Members)
        {
            Debug.Log($"teams : Name = {member.Name}, Address = {member.Address} ;");
        }

        Debug.Log($"teams : TeamName = {deepCopy.TeamName} ;");
        foreach (var member in deepCopy.Members)
        {
            Debug.Log($"deepCopy : Name = {member.Name}, Address = {member.Address} ;");
        }

・Teams.cs

using System;
using System.Collections.Generic;

/// <summary>
/// チームクラス
/// </summary>
[Serializable] // DeepCopy対象では Serializable 属性が必須
public class Teams
{
    public string TeamName;

    public List<Member> Members;
}


参考サイト

tomisenblog.com