Help us understand the problem. What is going on with this article?

C# 自作クラスListのコピー(Deep Copy)

コピーは1種類じゃない?

プログラミングにおける変数のコピーには
・ディープコピー
・シャローコピー
の2種類があります。
それぞれを一言でいうと

ディープコピー:コピー先の変数を変更しても、コピー元には影響がない

ディープコピーの例
int a = 1;
int b = a;
b = 2;
Console.WriteLine("a={0}",a);
Console.WriteLine("b={0}",b);
実行結果
a=1
b=2

上の例では、bを変えてもaには影響が出ていません

シャローコピー:コピー先の変数を変更すると、コピー元にも変更が適用される

シャローコピーの例
List<int> a = new List<int> { 0, 0, 0};
List<int> b = a;
b[1] = 1;
Console.Write("a=");
foreach(var member in a) Console.Write("{0} ",member);
Console.Write("\nb=");
foreach(var member in b) Console.Write("{0} ",member);
実行結果
a=0 1 0 
b=0 1 0

bを変えると、aも変わってしまっています。

C#におけるコピー

上の例を見て頂ければわかる通り、C#においては「=」で代入すると
int、doubleなどの値型:ディープコピー
配列、Listなどの参照型:シャローコピー
となるようです。

そもそものコピーの目的として
「元のデータを変えたくないからコピーしてるんだ!」
という理由が相当数を占めていると思うので、
基本的には
ディープコピーの需要が高い
と思っています。
しかしC#の標準ライブラリには汎用的にディープコピーするメソッドがないので、
実装してみました。
2通りの方法で実装しています。

実装例1:シリアライズを利用

こちらを参考にさせて頂きました。
参考というよりほぼコピペです。
ディープなコピペをしてしまい申し訳ない、、

ディープコピー用拡張メソッド
public static T DeepClone<T>(this T src)
    {
    using (var memoryStream = new System.IO.MemoryStream())
    {
        var binaryFormatter
          = new System.Runtime.Serialization
                .Formatters.Binary.BinaryFormatter();
        binaryFormatter.Serialize(memoryStream, src); // シリアライズ
        memoryStream.Seek(0, System.IO.SeekOrigin.Begin);
        return (T)binaryFormatter.Deserialize(memoryStream); // デシリアライズ
    }
}
検証用の自作クラス([Serializable]の記載を忘れるとエラーが出ます)
[Serializable]
class originalClass
{
    public int i;
    public string s;
    public List<int> iList;
}
実行例
List<originalClass> a = new List<originalClass>{
    new originalClass{ i = 0, s = "○", iList = new List<int> { 0, 0} },
    new originalClass{ i = 0, s = "○", iList = new List<int> { 0, 0} }};
List<originalClass> b = a.DeepClone();
b[0].i = 1;
b[0].iList[0] = 1;
Console.Write("a=");
foreach (var mem1 in a)
{
    Console.Write("{0} ", mem1.i);
    Console.Write("{0} ", mem1.s);
    Console.Write("{");
    foreach (var mem2 in mem1.iList) Console.Write("{0} ", mem2);
    Console.Write("}");
    Console.Write("\n");
}
Console.Write("\nb=");
foreach (var mem1 in b)
{
    Console.Write("{0} ", mem1.i);
    Console.Write("{0} ", mem1.s);
    Console.Write("{");
    foreach (var mem2 in mem1.iList) Console.Write("{0} ", mem2);
    Console.Write("}");
    Console.Write("\n");
}
```:実行結果
a=0 ○ {0 0 }
0 ○ {0 0 }

b=1 ○ {1 0 }
0 ○ {0 0 }

bを変更してもaは変わっていないので、正常にディープコピーできていそうです。

実装例2:コンストラクタを利用してnew

コンストラクタを適切に定義すれば、newによりディープコピーができるようです。
ただし、多重に参照している場合、値型にたどりつくまで下層を走査する必要があります。
詳しくは下記参考をご参照ください

検証用の自作クラス+コンストラクタ
class originalClass
{
    public int i;
    public string s;
    public List<int> iList;
    public originalClass(){}
    public originalClass(originalClass src)
    {
        i = src.i;
        s = src.s;
        iList = new List<int>();
        foreach(var mem in src.iList) iList.Add(mem);
    }
}
実行例
List<originalClass> a = new List<originalClass>{
    new originalClass{ i = 0, s = "○", iList = new List<int> { 0, 0} },
    new originalClass{ i = 0, s = "○", iList = new List<int> { 0, 0} }};
List<originalClass> b = new List<originalClass>();
foreach (var mem in a) b.Add(new originalClass(mem));
b[0].i = 1;
b[0].iList[0] = 1;
Console.Write("a=");
foreach (var mem1 in a)
{
    Console.Write("{0} ", mem1.i);
    Console.Write("{0} ", mem1.s);
    Console.Write("{");
    foreach (var mem2 in mem1.iList) Console.Write("{0} ", mem2);
    Console.Write("}");
    Console.Write("\n");
}
Console.Write("\nb=");
foreach (var mem1 in b)
{
    Console.Write("{0} ", mem1.i);
    Console.Write("{0} ", mem1.s);
    Console.Write("{");
    foreach (var mem2 in mem1.iList) Console.Write("{0} ", mem2);
    Console.Write("}");
    Console.Write("\n");
}
実行結果
a=0 ○ {0 0 }
0 ○ {0 0 }

b=1 ○ {1 0 }
0 ○ {0 0 }

bを変更してもaは変わっていないので、正常にディープコピーできていそうです。

参考:C#におけるシャローコピーとディープコピー

どんな場合がシャローコピー、どんな場合がディープコピーとなるか調べてみました

上の例を見ると、newすればコンストラクタが勝手にディープコピーしてくれるように見えます。
実行例を下記します。

値型のリストの場合

newによるListのディープコピー
List<int> a = new List<int> { 0, 0, 0};
List<int> b = new List<int>(a);
b[1] = 1;
Console.Write("a=");
foreach(var member in a) Console.Write("{0} ",member);
Console.Write("\nb=");
foreach(var member in b) Console.Write("{0} ",member);
実行結果
a=0 0 0 
b=0 1 0

bを変更してもaは変わっておらず、ディープコピーされているようです

多重リストの場合

次のような多重リストではどうでしょう?

newによる2重Listのコピー
List<List<int>> a = new List<List<int>>{
    new List<int> { 0, 0, 0 },
    new List<int> { 0, 0, 0 },
    new List<int> { 0, 0, 0 } };
List<List<int>> b = new List<List<int>>(a);
b[1][1] = 1;
Console.Write("a=");
foreach (var mem1 in a)
{
    foreach (var mem2 in mem1) Console.Write("{0} ", mem2);
    Console.Write("\n");
}
Console.Write("\nb=");
foreach (var mem1 in b)
{
    foreach (var mem2 in mem1) Console.Write("{0} ", mem2);
    Console.Write("\n");
}
実行結果
a=0 0 0
0 1 0
0 0 0

b=0 0 0
0 1 0
0 0 0

bと一緒にaまで変更されてしまっており、シャローコピーとなっているようです。
直下のリストはディープコピーされるが、そのリストが示す参照先はコピー元と共通、
といった感じと思われます。

自作クラスListの場合

データベース的な使い方をよくする、自作クラスListに対して、
よくありそうな3パターンに分けてコンストラクタによるnewの挙動を、
調べたいと思います。

newによるコピー結果検証用コード
List<originalClass> a = new List<originalClass>{
    new originalClass{ i = 0, s = "○", iList = new List<int> { 0, 0} },
    new originalClass{ i = 0, s = "○", iList = new List<int> { 0, 0} }};
List<originalClass> b = new List<originalClass>(a);
b[0].i = 1;
b[0].iList[0] = 1;
Console.Write("a=");
foreach (var mem1 in a)
{
    Console.Write("{0} ", mem1.i);
    Console.Write("{0} ", mem1.s);
    Console.Write("{");
    foreach (var mem2 in mem1.iList) Console.Write("{0} ", mem2);
    Console.Write("}");
    Console.Write("\n");
}
Console.Write("\nb=");
foreach (var mem1 in b)
{
    Console.Write("{0} ", mem1.i);
    Console.Write("{0} ", mem1.s);
    Console.Write("{");
    foreach (var mem2 in mem1.iList) Console.Write("{0} ", mem2);
    Console.Write("}");
    Console.Write("\n");
}


パターン1:コンストラクタ定義なし

クラス定義
class originalClass
{
    public int i;
    public string s;
    public List<int> iList;
}
実行結果
a=1 ○ {1 0 }
0 ○ {0 0 }

b=1 ○ {1 0 }
0 ○ {0 0 }

中身が値型だろうが容赦なしでシャローコピーされています



パターン2:コンストラクタ + foreachでコピー

クラスの定義
class originalClass
{
    public int i;
    public string s;
    public List<int> iList;
    public originalClass(){}
    public originalClass(originalClass src)
    {
        i = src.i;
        s = src.s;
        iList = src.iList;
    }
}
検証用コードの変更(コピー部分のみ抜粋)
List<originalClass> a = new List<originalClass>{
    new originalClass{ i = 0, s = "○", iList = new List<int> { 0, 0} },
    new originalClass{ i = 0, s = "○", iList = new List<int> { 0, 0} }};
List<originalClass> b = new List<originalClass>();
foreach (var mem in a) b.Add(new originalClass(mem));
b[0].i = 1;
b[0].iList[0] = 1;
Console.Write("a=");
foreach (var mem1 in a)
{
    Console.Write("{0} ", mem1.i);
    Console.Write("{0} ", mem1.s);
    Console.Write("{");
    foreach (var mem2 in mem1.iList) Console.Write("{0} ", mem2);
    Console.Write("}");
    Console.Write("\n");
}
Console.Write("\nb=");
foreach (var mem1 in b)
{
    Console.Write("{0} ", mem1.i);
    Console.Write("{0} ", mem1.s);
    Console.Write("{");
    foreach (var mem2 in mem1.iList) Console.Write("{0} ", mem2);
    Console.Write("}");
    Console.Write("\n");
}
実行結果
a=0 ○ {1 0 }
0 ○ {0 0 }

b=1 ○ {1 0 }
0 ○ {0 0 }

値型はディープコピー、参照型(List)はシャローコピーされています

パターン3:パターン2 + コンストラクタのリスト部分のみforeachで1要素ずつ代入

クラスの定義
class originalClass
{
    public int i;
    public string s;
    public List<int> iList;
    public originalClass(){}
    public originalClass(originalClass src)
    {
        i = src.i;
        s = src.s;
        iList = new List<int>();
        foreach(var mem in src.iList) iList.Add(mem);
    }
}
実行結果
a=0 ○ {0 0 }
0 ○ {0 0 }

b=1 ○ {1 0 }
0 ○ {0 0 }

ついにディープコピーが実現できました(bを変えてもaは変わらない)
このパターン3は、上の実装例2と同じものです。



結論としては、参照型が内部に存在する場合、値型にたどり着くまで内部を辿ってから代入しないといけないみたいです。
複雑な自作クラスになるとどこが値型なのか探索するのも、コンストラクタの実装の手間も大きそうなので、
実装1の方法がシンプルでよさそうです。

まとめ:ディープコピーになる場合とシャローコピーになる場合

※自作リストの場合は上記参照ください

C#においてディープコピーとなる例
//値型を代入
int b = a;
//stringの代入(stringは参照型だが、例外的にディープコピーとなる)
string b = a;
//値型のListをコンストラクタでnew
List<int> b = new List<int>(a);
C#においてシャローコピーとなる例
//配列、リスト等の参照型を代入
int[] b = a;
List<int> b = a;
//多重Listをコンストラクタでnew
List<List<int>> b = new List<List<int>>(a);
//foreachの中身(下の例でmemberを変更すると、aも変更される)
foreach(var member in a)
//gourpbyの中身(下の例でgroupaを変更すると、aも変更される)
var groupa = a.Groupby(c => c.key)

foreachやgroupbyは、逆にディープコピーだとループ内での変更ができず困る場面もありそうなので、
シャローコピーで助かる面も多いかと思います。

c60evaporator
おうちの聖域なきIoT化を進めています
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
No comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
ユーザーは見つかりませんでした