Quantcast
Channel: C#タグが付けられた新着記事 - Qiita
Viewing all articles
Browse latest Browse all 9333

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

$
0
0

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

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

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

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

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

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

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

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

C#におけるコピー

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

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

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

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

ディープコピー用拡張メソッド
publicstaticTDeepClone<T>(thisTsrc){using(varmemoryStream=newSystem.IO.MemoryStream()){varbinaryFormatter=newSystem.Runtime.Serialization.Formatters.Binary.BinaryFormatter();binaryFormatter.Serialize(memoryStream,src);// シリアライズmemoryStream.Seek(0,System.IO.SeekOrigin.Begin);return(T)binaryFormatter.Deserialize(memoryStream);// デシリアライズ}}
検証用の自作クラス([Serializable]の記載を忘れるとエラーが出ます)
[Serializable]classoriginalClass{publicinti;publicstrings;publicList<int>iList;}
実行例
List<originalClass>a=newList<originalClass>{neworiginalClass{i=0,s="○",iList=newList<int>{0,0}},neworiginalClass{i=0,s="○",iList=newList<int>{0,0}}};List<originalClass>b=a.DeepClone();b[0].i=1;b[0].iList[0]=1;Console.Write("a=");foreach(varmem1ina){Console.Write("{0} ",mem1.i);Console.Write("{0} ",mem1.s);Console.Write("{");foreach(varmem2inmem1.iList)Console.Write("{0} ",mem2);Console.Write("}");Console.Write("\n");}Console.Write("\nb=");foreach(varmem1inb){Console.Write("{0} ",mem1.i);Console.Write("{0} ",mem1.s);Console.Write("{");foreach(varmem2inmem1.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によりディープコピーができるようです。
ただし、多重に参照している場合、値型にたどりつくまで下層を走査する必要があります。
詳しくは下記参考をご参照ください

検証用の自作クラス+コンストラクタ
classoriginalClass{publicinti;publicstrings;publicList<int>iList;publicoriginalClass(){}publicoriginalClass(originalClasssrc){i=src.i;s=src.s;iList=newList<int>();foreach(varmeminsrc.iList)iList.Add(mem);}}
実行例
List<originalClass>a=newList<originalClass>{neworiginalClass{i=0,s="○",iList=newList<int>{0,0}},neworiginalClass{i=0,s="○",iList=newList<int>{0,0}}};List<originalClass>b=newList<originalClass>();foreach(varmemina)b.Add(neworiginalClass(mem));b[0].i=1;b[0].iList[0]=1;Console.Write("a=");foreach(varmem1ina){Console.Write("{0} ",mem1.i);Console.Write("{0} ",mem1.s);Console.Write("{");foreach(varmem2inmem1.iList)Console.Write("{0} ",mem2);Console.Write("}");Console.Write("\n");}Console.Write("\nb=");foreach(varmem1inb){Console.Write("{0} ",mem1.i);Console.Write("{0} ",mem1.s);Console.Write("{");foreach(varmem2inmem1.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=newList<int>{0,0,0};List<int>b=newList<int>(a);b[1]=1;Console.Write("a=");foreach(varmemberina)Console.Write("{0} ",member);Console.Write("\nb=");foreach(varmemberinb)Console.Write("{0} ",member);
実行結果
a=0 0 0 
b=0 1 0

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

多重リストの場合

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

newによる2重Listのコピー
List<List<int>>a=newList<List<int>>{newList<int>{0,0,0},newList<int>{0,0,0},newList<int>{0,0,0}};List<List<int>>b=newList<List<int>>(a);b[1][1]=1;Console.Write("a=");foreach(varmem1ina){foreach(varmem2inmem1)Console.Write("{0} ",mem2);Console.Write("\n");}Console.Write("\nb=");foreach(varmem1inb){foreach(varmem2inmem1)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=newList<originalClass>{neworiginalClass{i=0,s="○",iList=newList<int>{0,0}},neworiginalClass{i=0,s="○",iList=newList<int>{0,0}}};List<originalClass>b=newList<originalClass>(a);b[0].i=1;b[0].iList[0]=1;Console.Write("a=");foreach(varmem1ina){Console.Write("{0} ",mem1.i);Console.Write("{0} ",mem1.s);Console.Write("{");foreach(varmem2inmem1.iList)Console.Write("{0} ",mem2);Console.Write("}");Console.Write("\n");}Console.Write("\nb=");foreach(varmem1inb){Console.Write("{0} ",mem1.i);Console.Write("{0} ",mem1.s);Console.Write("{");foreach(varmem2inmem1.iList)Console.Write("{0} ",mem2);Console.Write("}");Console.Write("\n");}


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

クラス定義
classoriginalClass{publicinti;publicstrings;publicList<int>iList;}
実行結果
a=1 ○ {1 0 }
0 ○ {0 0 }

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

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



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

クラスの定義
classoriginalClass{publicinti;publicstrings;publicList<int>iList;publicoriginalClass(){}publicoriginalClass(originalClasssrc){i=src.i;s=src.s;iList=src.iList;}}
検証用コードの変更(コピー部分のみ抜粋)
List<originalClass>a=newList<originalClass>{neworiginalClass{i=0,s="○",iList=newList<int>{0,0}},neworiginalClass{i=0,s="○",iList=newList<int>{0,0}}};List<originalClass>b=newList<originalClass>();foreach(varmemina)b.Add(neworiginalClass(mem));b[0].i=1;b[0].iList[0]=1;Console.Write("a=");foreach(varmem1ina){Console.Write("{0} ",mem1.i);Console.Write("{0} ",mem1.s);Console.Write("{");foreach(varmem2inmem1.iList)Console.Write("{0} ",mem2);Console.Write("}");Console.Write("\n");}Console.Write("\nb=");foreach(varmem1inb){Console.Write("{0} ",mem1.i);Console.Write("{0} ",mem1.s);Console.Write("{");foreach(varmem2inmem1.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要素ずつ代入

クラスの定義
classoriginalClass{publicinti;publicstrings;publicList<int>iList;publicoriginalClass(){}publicoriginalClass(originalClasssrc){i=src.i;s=src.s;iList=newList<int>();foreach(varmeminsrc.iList)iList.Add(mem);}}
実行結果
a=0 ○ {0 0 }
0 ○ {0 0 }

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

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



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

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

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

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

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


Viewing all articles
Browse latest Browse all 9333

Latest Images

Trending Articles