参照型変数(主にクラスのオブジェクト)をコピーする場合、単純に「=」で代入するだけでは不十分な場合があります。
参照型変数の中身は参照先(オブジェクトのメモリ領域を示すポインタ)です。
「=」で代入するだけでは、参照先だけがコピーされて、参照しているものは同じという状態になるので、コピー先の変更がコピー元に影響してしまいますし、その逆にコピー元の変更がコピー先に影響してしまいます。
これを避けたい場合は、MemberwiseCloneメソッドを用いて中身を丸ごとコピー(新たにメモリ領域を確保し書き込み、ディープコピー)する必要があります。
MemberwiseCloneメソッドはobject型で定義されており、C#ではすべての型はobject型から派生しているので、特別な記述を行わなくともMemberwiseCloneメソッドを使用できます。ただし、戻り値はobject型なので、キャスト等の考慮は必要です。
(javaの場合はこちらの記事のようにインターフェースの実装が必要だったり例外処理が必要だったりと色々面倒です。後発言語であるC#では言語仕様上ディープコピーを始めから考慮している印象を受けます。)
以下、サンプルコードです。
参照先のみコピーした場合とMemberwiseCloneメソッドで中身をコピーした場合を比較しています。
参照先のみコピーした場合は、コピー後にコピー先を変更した際にコピー元が影響を受けていますが、MemberwiseCloneメソッドで中身をコピーした場合は影響を受けていません。
【サンプルコード】
・CloneableItem.cs
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; namespace CloneTest { class CloneableItem { // ミュータブルの参照型変数を含む場合はそれも一緒にcloneが必要 // javaの記事では書き忘れたがシリアライズ・デシリアライズでも良い // (今回は割愛) public int ItemId { get; set; } public string ItemName { get; set; } // MemberwiseCloneメソッドを使用 public CloneableItem Clone() { // Object型で返ってくるのでキャストが必要 return (CloneableItem)MemberwiseClone(); } } } |
・ItemCloneMain.cs
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 |
using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; namespace CloneTest { class ItemCloneMain { static void Main(string[] args) { Console.WriteLine("[参照先のみコピーした場合]"); CloneableItem cloneableItem1 = new CloneableItem(); cloneableItem1.ItemId = 1; cloneableItem1.ItemName = "sword"; Console.WriteLine ("コピー前のコピー元オブジェクト:ID=" + cloneableItem1.ItemId + " NAME=" + cloneableItem1.ItemName); CloneableItem cloneableItem2 = cloneableItem1; Console.WriteLine ("コピー後のコピー元オブジェクト:ID=" + cloneableItem1.ItemId + " NAME=" + cloneableItem1.ItemName); Console.WriteLine ("コピー後のコピー先オブジェクト:ID=" + cloneableItem2.ItemId + " NAME=" + cloneableItem2.ItemName); cloneableItem2.ItemId = 2; cloneableItem2.ItemName = "shield"; Console.WriteLine ("編集後のコピー元オブジェクト :ID=" + cloneableItem1.ItemId + " NAME=" + cloneableItem1.ItemName); Console.WriteLine ("編集後のコピー先オブジェクト :ID=" + cloneableItem2.ItemId + " NAME=" + cloneableItem2.ItemName); Console.WriteLine("[参照先のみコピーした場合]"); CloneableItem cloneableItem3 = new CloneableItem(); cloneableItem3.ItemId = 3; cloneableItem3.ItemName = "armor"; Console.WriteLine ("コピー前のコピー元オブジェクト:ID=" + cloneableItem3.ItemId + " NAME=" + cloneableItem3.ItemName); CloneableItem cloneableItem4 = cloneableItem3.Clone(); Console.WriteLine ("コピー後のコピー元オブジェクト:ID=" + cloneableItem3.ItemId + " NAME=" + cloneableItem3.ItemName); Console.WriteLine ("コピー後のコピー先オブジェクト:ID=" + cloneableItem4.ItemId + " NAME=" + cloneableItem4.ItemName); cloneableItem4.ItemId = 4; cloneableItem4.ItemName = "helm"; Console.WriteLine ("編集後のコピー元オブジェクト :ID=" + cloneableItem3.ItemId + " NAME=" + cloneableItem3.ItemName); Console.WriteLine ("編集後のコピー先オブジェクト :ID=" + cloneableItem4.ItemId + " NAME=" + cloneableItem4.ItemName); Console.ReadKey(true); } } } |
【実行結果】
1 2 3 4 5 6 7 8 9 10 11 12 |
[参照先のみコピーした場合] コピー前のコピー元オブジェクト:ID=1 NAME=sword コピー後のコピー元オブジェクト:ID=1 NAME=sword コピー後のコピー先オブジェクト:ID=1 NAME=sword 編集後のコピー元オブジェクト :ID=2 NAME=shield 編集後のコピー先オブジェクト :ID=2 NAME=shield [参照先のみコピーした場合] コピー前のコピー元オブジェクト:ID=3 NAME=armor コピー後のコピー元オブジェクト:ID=3 NAME=armor コピー後のコピー先オブジェクト:ID=3 NAME=armor 編集後のコピー元オブジェクト :ID=3 NAME=armor 編集後のコピー先オブジェクト :ID=4 NAME=helm |
いかがでしたでしょうか?
C#は後発言語であるため、記述を簡略化できる場面が多いです。
今回紹介したディープコピーの例もそうですし、getter・setterの記法もC#だと簡略化できます(今回のサンプルコード中にも出てきます)。
javaを触った後にC#を触ると、こうした細かい所で便利さを感じます。
これからも、C#に関する記事を投稿していきたいと思います!
コメント