オブジェクトをコピーしてデータを編集する。たとえば「保存」ボタンをクリックしたときに、編集データの保存かキャンセルを選べるような場合に有効ですが、使い方を誤ると思わぬ事故を誘発します。
事故の多くは浅いコピー(シャローコピー/Shallow Copy)と深いコピー(ディープコピー/Deep Copy)を間違えて「本来変わってはいけない値が変わってしまう」というものです。
そこで本稿ではオブジェクトの複製についてまとめていきたいと思います。
はじめに「会員」の情報が以下のように定義されていたとします。
●会員情報のクラス図
●アカウントクラス(Account)のソース
●会員クラス(Membership)のソース
オブジェクトのコピーで一番簡単な方法は「代入」です。ためしに以下のコードを実行してみましょう。
// 会員1を生成
Membership membership1 = new Membership();
// 会員1のIDとパスワード、ニックネームを設定
membership1.ID = 1;
membership1.Password = "1234567890";
membership1.Nickname = "変数名.com";
// 会員2に会員1を代入
Membership membership2 = membership1;
// 会員1の情報を表示
Console.WriteLine("会員1 [{0}][{1}][{2}]",
membership1.ID, membership1.Password, membership1.Nickname);
// 会員2の情報を表示
Console.WriteLine("会員2 [{0}][{1}][{2}]",
membership2.ID, membership2.Password, membership2.Nickname);
Console.WriteLine();
// 会員1の情報を変更
membership1.ID = 999;
membership1.Password = "ABCDEFGHIJ";
membership1.Nickname = "リファクタ";
// 会員1の情報を表示
Console.WriteLine("会員1 [{0}][{1}][{2}]",
membership1.ID, membership1.Password, membership1.Nickname);
// 会員2の情報を表示
Console.WriteLine("会員2 [{0}][{1}][{2}]",
membership2.ID, membership2.Password, membership2.Nickname);
会員1 [1][1234567890][変数名.com]
会員2 [1][1234567890][変数名.com]
会員1 [999][ABCDEFGHIJ][リファクタ]
会員2 [999][ABCDEFGHIJ][リファクタ]
会員1の情報だけを変えたつもりが、会員2の情報まで変わってしまいました。これは「membership1」と「membership2」の参照先が同じなので、会員1への変更が会員2にも反映されてしまったことが原因です。
会員1への変更が会員2にも反映されてしまった。。
それでは会員クラス(Membership)に実装した「Clone」メソッドでコピーしてみましょう。
// 会員1を生成
Membership membership1 = new Membership();
// 会員1のIDとパスワード、ニックネームを設定
membership1.ID = 1;
membership1.Password = "1234567890";
membership1.Nickname = "変数名.com";
// 会員2に会員1のシャローコピーを代入
Membership membership2 = membership1.Clone();
// 会員1の情報を表示
Console.WriteLine("会員1 [{0}][{1}][{2}]",
membership1.ID, membership1.Password, membership1.Nickname);
// 会員2の情報を表示
Console.WriteLine("会員2 [{0}][{1}][{2}]",
membership2.ID, membership2.Password, membership2.Nickname);
Console.WriteLine();
// 会員1の情報を変更
membership1.ID = 999;
membership1.Password = "ABCDEFGHIJ";
membership1.Nickname = "リファクタ";
// 会員1の情報を表示
Console.WriteLine("会員1 [{0}][{1}][{2}]",
membership1.ID, membership1.Password, membership1.Nickname);
// 会員2の情報を表示
Console.WriteLine("会員2 [{0}][{1}][{2}]",
membership2.ID, membership2.Password, membership2.Nickname);
会員1 [1][1234567890][変数名.com]
会員2 [1][1234567890][変数名.com]
会員1 [999][ABCDEFGHIJ][リファクタ]
会員2 [999][ABCDEFGHIJ][変数名.com]
ニックネームは会員1だけ変わりましたが、アカウントの情報は会員2も変わってしまいました。これは会員クラス(Membership)に実装した「Clone」メソッドで使用している「MemberwiseClone」メソッドが、浅いコピーを作成することが原因です。
浅いコピーは値型のフィールドをビット単位でコピーしますが、参照型のフィールドは参照のみをコピーします。
会員1のアカウント情報の変更が会員2にも反映されてしまった。。
参照型のフィールドもビット単位でコピーするためには、深いコピーを作成する必要があります。
深いコピーを作成する方法の一つにシリアライズを利用する方法があり、私はこの方法が一番わかりやすくて気に入っています。
シリアライズを利用するためには、クラスに[Serializable]属性を付与する必要があります。アカウントクラス(Account)と会員クラス(Membership)に付与し、続いて「Clone」メソッドをシリアライズを利用して書き換えてみましょう。
●アカウントクラス(Account)のソース
●会員クラス(Membership)のソース
それでは会員クラス(Membership)に実装した「Clone」メソッドでもう一度コピーしてみましょう。
// 会員1を生成
Membership membership1 = new Membership();
// 会員1のIDとパスワード、ニックネームを設定
membership1.ID = 1;
membership1.Password = "1234567890";
membership1.Nickname = "変数名.com";
// 会員2に会員1のディープコピーを代入
Membership membership2 = membership1.Clone();
// 会員1の情報を表示
Console.WriteLine("会員1 [{0}][{1}][{2}]",
membership1.ID, membership1.Password, membership1.Nickname);
// 会員2の情報を表示
Console.WriteLine("会員2 [{0}][{1}][{2}]",
membership2.ID, membership2.Password, membership2.Nickname);
Console.WriteLine();
// 会員1の情報を変更
membership1.ID = 999;
membership1.Password = "ABCDEFGHIJ";
membership1.Nickname = "リファクタ";
// 会員1の情報を表示
Console.WriteLine("会員1 [{0}][{1}][{2}]",
membership1.ID, membership1.Password, membership1.Nickname);
// 会員2の情報を表示
Console.WriteLine("会員2 [{0}][{1}][{2}]",
membership2.ID, membership2.Password, membership2.Nickname);
会員1 [1][1234567890][変数名.com]
会員2 [1][1234567890][変数名.com]
会員1 [999][ABCDEFGHIJ][リファクタ]
会員2 [1][1234567890][変数名.com]
今度は無事に会員1の情報だけが変更されました。
会員1の情報だけが変更された!
このように深いコピーはいいことずくめのように見えますが、クラスの内容を一旦ストリームに吐き出し、そのストリームから再度クラスを生成しているので、処理は当然遅くなります。
何でもかんでも深いコピーを利用するのではなく、参照系は浅いコピー、更新系は深いコピーを利用するなど、ケースバイケースで使い分けることをおススメします。