#コピーは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]
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すればコンストラクタが勝手にディープコピーしてくれるように見えます。
実行例を下記します。
##値型のリストの場合
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は変わっておらず、ディープコピーされているようです
##多重リストの場合
次のような多重リストではどうでしょう?
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の挙動を、
調べたいと思います。
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");
}
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の方法がシンプルでよさそうです。
##まとめ:ディープコピーになる場合とシャローコピーになる場合
※自作リストの場合は上記参照ください
//値型を代入
int b = a;
//stringの代入(stringは参照型だが、例外的にディープコピーとなる)
string b = a;
//値型のListをコンストラクタでnew
List<int> b = new List<int>(a);
//配列、リスト等の参照型を代入
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は、逆にディープコピーだとループ内での変更ができず困る場面もありそうなので、
シャローコピーで助かる面も多いかと思います。