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

シャローコピー

プログラミングではオブジェクトをコピーする操作が頻繁に行われます。
最も基本的なコピーはシャローコピーと呼ばれるもので、代入操作や配列のArray.Copyメソッドなどはシャローコピーとなります。
(shallow copy=浅いコピー。簡易コピーとも呼ばれる)
シャローコピーは、変数などのオブジェクトに入っている値をそのままコピー先に複製します。

コピーするデータが値型の場合は特に問題はありませんが、参照型の場合は注意が必要です。
参照型のオブジェクトの中身は参照先(メモリ上の位置やサイズなど)を表すデータ(アドレス)です。
参照型のシャローコピーは「参照先はどこか」という情報の複製になるので、複製元オブジェクトと複製先オブジェクトが同じ場所を参照している状態になります。


//クラスは参照型
class SimpleClass
{
    public int X;
    public int Y;

    public SimpleClass(int x = 0, int y = 0)
    {
        X = x;
        Y = y;
    }
}

SimpleClass sc1 = new SimpleClass(1);

//sc2はsc1と同じ場所を指すようになる
SimpleClass sc2 = sc1;

//コピー元を書き換えてみる
sc1.X = 2;

Console.WriteLine("{0}, {1}", sc1.X, sc2.X);
2 2

上記の場合、インスタンスsc1とsc2はメモリ上の同じ位置を指すようになります。
同じ場所を指しているので、一方のデータの書き換えはもう一方にも影響します。

値型と参照型も参照してください。

ディープコピー

同じ場所を参照するのでは困る、という場合はディープコピー(深いコピー)をする必要があります。
ディープコピーは参照先のデータも含めて別のメモリ領域に複製するコピー方法です。

C#にはディープコピーを一発で行うようなメソッドは標準では用意されていないため、自分で書く必要があります。
最も単純な方法としては、別の新しいオブジェクトを生成して値を一つずつコピーしていく方法です。


SimpleClass sc1 = new SimpleClass(1);
SimpleClass sc2 = new SimpleClass();

sc2.X = sc1.X;
sc2.Y = sc1.Y;

このような処理はコピーコンストラクターで用意しておくと便利です。


class SimpleClass
{
    public int X;
    public int Y;

    public SimpleClass(int x = 0, int y = 0)
    {
        X = x;
        Y = y;
    }

    //コピーコンストラクタ
    public SimpleClass(SimpleClass sc)
    {
        X = sc.X;
        Y = sc.Y;
    }
}

SimpleClass sc1 = new SimpleClass(1);
SimpleClass sc2 = new SimpleClass(sc1);

ただし代入はシャローコピーなので、メンバーに参照型がある場合はディープコピーにはなりません。
(後述)

シャローコピーの実装

クラスにシャローコピーを実装するには、新しいオブジェクトを生成してすべてのメンバーを単純に代入するだけで良いです。
しかしメンバーの数が多いと記述が大変なので、簡単に実装するにはMemberwiseCloneメソッドを利用します。

MemberwiseCloneメソッドはobject型で定義されています。
C#ではすべての型はobject型から派生して作られており、自作クラスでもobject型で定義されているメソッドを使用できます。


class SimpleClass
{
    public int X;
    public int Y;

    public SimpleClass(int x = 0, int y = 0)
    {
        X = x;
        Y = y;
    }

    public SimpleClass ShallowCopy()
    {
        return (SimpleClass)MemberwiseClone();
    }
}

SimpleClass sc1 = new SimpleClass(1);
SimpleClass sc2 = sc1.ShallowCopy();

sc1.X = 2;

Console.WriteLine("{0} {1}", sc1.X, sc2.X);
2 1

MemberwiseCloneメソッドは、メンバーをすべてシャローコピーした新しいオブジェクトを返します。
戻り値はobject型なのでキャストして返す必要があります。
このメソッドはprotectedで定義されているので、クラスの外部から呼び出して使用することはできません。

インスタンス同士の単純な代入とは違い、新しいオブジェクトを生成して返すので、一方のメンバーを書き換えてももう一方には影響しません。

メンバーがすべて値型であればこれで十分です。
しかしメンバーのコピーはシャローコピーなので、メンバーに参照型のオブジェクトがある場合はメモリ上の同じ位置を指したままとなります。


class SimpleClass
{
    public int Num;

    //配列は参照型
    public int[] Arr;

    public SimpleClass(int length)
    {
        Num = length;

        //0~lengthまでを要素とする配列を得る
        Arr = Enumerable.Range(0, length).ToArray();
    }

    public SimpleClass ShallowCopy()
    {
        return (SimpleClass)MemberwiseClone();
    }
}

SimpleClass sc1 = new SimpleClass(3);
SimpleClass sc2 = sc1.ShallowCopy();

Console.WriteLine("sc1: {0} {1}", sc1.Num, sc1.Arr[0]);
Console.WriteLine("sc2: {0} {1}", sc2.Num, sc2.Arr[0]);
Console.WriteLine();

//コピー元を書き換えてみる
sc1.Num = 9;
sc1.Arr[0] = 9;

Console.WriteLine("sc1: {0} {1}", sc1.Num, sc1.Arr[0]);
Console.WriteLine("sc2: {0} {1}", sc2.Num, sc2.Arr[0]);
sc1: 3 0
sc2: 3 0

sc1: 9 9
sc2: 3 9

参照型メンバーである配列の要素を書き換えると、他方にも影響していることがわかります。

ただし参照型でもstring型は値型に近い振舞いとなるので、シャローコピーで問題ありません。
C#での文字列の書き換えは、実際には書き換えではなく新しい文字列を生成して返すので、「元データが書き換えられる」ということが起こらないためです。


class SimpleClass
{
    public string Str;

    public SimpleClass(string s)
    {
        Str = s;
    }

    public SimpleClass ShallowCopy()
    {
        return (SimpleClass)MemberwiseClone();
    }
}

SimpleClass sc1 = new SimpleClass("abc");
SimpleClass sc2 = sc1.ShallowCopy();

//こういうのは書けない
//sc1.Str[0] = 'z';

//別の文字列に置き換えはOK
sc1.Str = "def";

Console.WriteLine("{0} {1}", sc1.Str, sc2.Str);
def abc

ディープコピーの実装

参照型だけ個別にコピー

ディープコピーを実装するには、メンバーをMemberwiseCloneメソッドでシャローコピーした後、参照型のメンバーだけを個別にコピーする方法があります。


class SimpleClass
{
    public int Num;
    public int[] Arr;

    public SimpleClass(int length)
    {
        Num = length;
        Arr = Enumerable.Range(0, length).ToArray();
    }

    public SimpleClass ShallowCopy()
    {
        return (SimpleClass)MemberwiseClone();
    }

    public SimpleClass DeepCopy()
    {
        SimpleClass sc = ShallowCopy();
        if(sc.Arr != null)
        {
            Arr = new int[sc.Arr.Length];
            Array.Copy(sc.Arr, Arr, sc.Arr.Length);
        }
        return sc;
    }
}

SimpleClass sc1 = new SimpleClass(3);
SimpleClass sc2 = sc1.DeepCopy();

Console.WriteLine("sc1: {0} {1}", sc1.Num, sc1.Arr[0]);
Console.WriteLine("sc2: {0} {1}", sc2.Num, sc2.Arr[0]);
Console.WriteLine();

//コピー元を書き換えてみる
sc1.Num = 9;
sc1.Arr[0] = 9;

Console.WriteLine("sc1: {0} {1}", sc1.Num, sc1.Arr[0]);
Console.WriteLine("sc2: {0} {1}", sc2.Num, sc2.Arr[0]);
sc1: 3 0
sc2: 3 0

sc1: 9 9
sc2: 3 0

参照型のメンバーの中身がさらに参照型の場合、同様の処理をコピー対象が参照型でなくなるまで続ける必要があります。

シリアライズを利用する

BinaryFormatterクラスなどでシリアライズした後にデシリアライズすることで参照型も含めてすべてディープコピーしたオブジェクトを得ることができます。


using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;

//↓追加しておく
using System.Runtime.Serialization.Formatters.Binary;
using System.IO;

[Serializable]
class SimpleClass
{
    public int Num;
    public int[] Arr;

    public SimpleClass(int length)
    {
        Num = length;
        Arr = Enumerable.Range(0, length).ToArray();
    }

    public SimpleClass DeepCopy()
    {
        using (MemoryStream ms = new MemoryStream())
        {
            BinaryFormatter bf = new BinaryFormatter();
            bf.Serialize(ms, this);
            ms.Position = 0;
            return (SimpleClass)bf.Deserialize(ms);
        }
    }
}

SimpleClass sc1 = new SimpleClass(3);
SimpleClass sc2 = sc1.DeepCopy();

Console.WriteLine("sc1: {0} {1}", sc1.Num, sc1.Arr[0]);
Console.WriteLine("sc2: {0} {1}", sc2.Num, sc2.Arr[0]);
Console.WriteLine();

//コピー元を書き換えてみる
sc1.Num = 9;
sc1.Arr[0] = 9;

Console.WriteLine("sc1: {0} {1}", sc1.Num, sc1.Arr[0]);
Console.WriteLine("sc2: {0} {1}", sc2.Num, sc2.Arr[0]);
sc1: 3 0
sc2: 3 0

sc1: 9 9
sc2: 3 0

この方法はメンバーを個別にコピーする必要がないため手軽です。
ジェネリック拡張メソッドを利用すればシリアライズが可能なクラスをディープコピーするメソッドを作ることもできます。


public static class ClassExtension
{   
    /// <summary>
    /// シリアライズ可能なクラスをディープコピーする。
    /// </summary>
    /// <typeparam name="T">コピー対象のクラス</typeparam>
    /// <param name="src">コピーするインスタンス</param>
    /// srcのディープコピー。
    /// クラスにSerializable属性が付いていなければnull。
    public static T DeepCopy<T>(this T src) where T : class
    {
        //ディープコピー不可ならnullを返す
        if (!HasAttribute<T, System.SerializableAttribute>())
            return null;

        using (var ms = new System.IO.MemoryStream())
        {
            var bf = new System.Runtime.Serialization.Formatters.Binary.BinaryFormatter();
            bf.Serialize(ms, src);
            ms.Position = 0;
            return (T)bf.Deserialize(ms);
        }
    }

    /// <summary>
    /// クラスに指定の属性が存在するかをチェックする。
    /// </summary>
    /// <typeparam name="T">チェック対象のクラス</typeparam>
    /// <typeparam name="Attr">チェックする属性</typeparam>
    /// <returns>Attr属性の有無。</returns>
    public static bool HasAttribute<T, Attr>()
        where T : class
        where Attr : System.Attribute
    {
        //Serializable属性が付いていなければfalseを返す
        return System.Attribute.GetCustomAttribute(
                typeof(T), typeof(Attr)
            ) != null;
    }
}

SimpleClass sc1 = new SimpleClass(3);
SimpleClass sc2 = sc1.DeepCopy();
if(sc2 == null)
{
    Console.WriteLine("SimpleClassにSerializable属性が付いていない");
    Console.ReadLine();
    return;
}