タグ : Deep Copy

 

.NET 全てのオブジェクトをコピーする汎用ディープコピーメソッド

前回投稿した「浅いコピーと深いコピー」では、データクラスにディープコピーメソッドを実装しましたが、全てのデータクラスに深いコピーが必要なわけではありません。
 
汎用のディープコピーメソッドを作成しておくと、必要に応じてオブジェクトをコピーできるので便利です。
 
ディープコピーメソッドのソースコードは以下になります。
 
using System;
using System.IO;
using System.Runtime.Serialization.Formatters.Binary;

namespace ShallowCopyTest
{
    /// <summary>
    /// コピーユーティリティ
    /// </summary>
    public static class CopyUtility
    {
        /// <summary>
        /// 指定されたインスタンスの深いコピーを生成し返します
        /// </summary>
        /// <typeparam name="T">コピーするインスタンスの型</typeparam>
        /// <param name="original">コピー元のインスタンス</param>
        /// <returns>指定されたインスタンスの深いコピー</returns>
        public static T DeepCopy<T>(T original)
        {
            // シリアル化した内容を保持するメモリーストリームを生成
            MemoryStream stream = new MemoryStream();

            try
            {
                // バイナリ形式でシリアライズするためのフォーマッターを生成
                BinaryFormatter formatter = new BinaryFormatter();

                // コピー元のインスタンスをシリアライズ
                formatter.Serialize(stream, original);

                // メモリーストリームの現在位置を先頭に設定
                stream.Position = 0L;

                // メモリーストリームの内容を逆シリアル化
                return (T)formatter.Deserialize(stream);
            }
            finally
            {
                stream.Close();
            }
        }
    }
}
 
 
それでは前回の会員クラス(Membership)をディープコピーしてみましょう。
 
    // 会員1を生成
    Membership membership1 = new Membership();

    // 会員1のIDとパスワード、ニックネームを設定
    membership1.ID = 1;
    membership1.Password = "1234567890";
    membership1.Nickname = "変数名.com";

    // 会員2に会員1のディープコピーを代入
    Membership membership2 =
        CopyUtility.DeepCopy<Membership>(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 [1][1234567890][変数名.com]
 
無事に会員1の情報だけが変更されました。
 

浅いコピーと深いコピー

オブジェクトをコピーしてデータを編集する。たとえば「保存」ボタンをクリックしたときに、編集データの保存かキャンセルを選べるような場合に有効ですが、使い方を誤ると思わぬ事故を誘発します。
 
事故の多くは浅いコピー(シャローコピー/Shallow Copy)と深いコピー(ディープコピー/Deep Copy)を間違えて「本来変わってはいけない値が変わってしまう」というものです。
 
そこで本稿ではオブジェクトの複製についてまとめていきたいと思います。
 
はじめに「会員」の情報が以下のように定義されていたとします。
 
●会員情報のクラス図
会員情報
●アカウントクラス(Account)のソース
using System;

namespace ShallowCopyTest
{
    /// <summary>
    /// アカウント
    /// </summary>
    public class Account
    {
        /// <summary>
        /// ID
        /// </summary>
        public int ID;

        /// <summary>
        /// パスワード
        /// </summary>
        public string Password;

        /// <summary>
        /// コンストラクタ
        /// </summary>
        public Account()
        {
        }
    }
}
 
●会員クラス(Membership)のソース
using System;

namespace ShallowCopyTest
{
    /// <summary>
    /// 会員
    /// </summary>
    public class Membership
    {
        /// <summary>
        /// アカウント
        /// </summary>
        private Account _account = new Account();

        /// <summary>
        /// ID
        /// </summary>
        public int ID
        {
            get { return this._account.ID; }
            set { this._account.ID = value; }
        }

        /// <summary>
        /// パスワード
        /// </summary>
        public string Password
        {
            get { return this._account.Password; }
            set { this._account.Password = value; }
        }

        /// <summary>
        /// ニックネーム
        /// </summary>
        public string Nickname;

        /// <summary>
        /// コンストラクタ
        /// </summary>
        public Membership()
        {
        }

        /// <summary>
        /// 現在のインスタンスのコピーを作成します
        /// </summary>
        /// <returns>現在のインスタンスのコピー</returns>
        public Membership Clone()
        {
            return (Membership)MemberwiseClone();
        }
    }
}
 
オブジェクトのコピーで一番簡単な方法は「代入」です。ためしに以下のコードを実行してみましょう。
 
        // 会員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にも反映されてしまった。。

会員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にも反映されてしまった。。

会員1のアカウント情報の変更が会員2にも反映されてしまった。。

 
参照型のフィールドもビット単位でコピーするためには、深いコピーを作成する必要があります。
 
深いコピーを作成する方法の一つにシリアライズを利用する方法があり、私はこの方法が一番わかりやすくて気に入っています。
 
シリアライズを利用するためには、クラスに[Serializable]属性を付与する必要があります。アカウントクラス(Account)と会員クラス(Membership)に付与し、続いて「Clone」メソッドをシリアライズを利用して書き換えてみましょう。
 
●アカウントクラス(Account)のソース
using System;

namespace ShallowCopyTest
{
    /// <summary>
    /// アカウント
    /// </summary>
    [Serializable]
    public class Account
    {
        /// <summary>
        /// ID
        /// </summary>
        public int ID;

        /// <summary>
        /// パスワード
        /// </summary>
        public string Password;

        /// <summary>
        /// コンストラクタ
        /// </summary>
        public Account()
        {
        }
    }
}
 
●会員クラス(Membership)のソース
using System;
using System.IO;
using System.Runtime.Serialization.Formatters.Binary;

namespace ShallowCopyTest
{
    /// <summary>
    /// 会員
    /// </summary>
    [Serializable]
    public class Membership
    {
        /// <summary>
        /// アカウント
        /// </summary>
        private Account _account = new Account();

        /// <summary>
        /// ID
        /// </summary>
        public int ID
        {
            get { return this._account.ID; }
            set { this._account.ID = value; }
        }

        /// <summary>
        /// パスワード
        /// </summary>
        public string Password
        {
            get { return this._account.Password; }
            set { this._account.Password = value; }
        }

        /// <summary>
        /// ニックネーム
        /// </summary>
        public string Nickname;

        /// <summary>
        /// コンストラクタ
        /// </summary>
        public Membership()
        {
        }

        /// <summary>
        /// 現在のインスタンスのコピーを作成します
        /// </summary>
        /// <returns>現在のインスタンスのコピー</returns>
        public Membership Clone()
        {
            // シリアル化した内容を保持するメモリーストリームを生成
            MemoryStream stream = new MemoryStream();

            try
            {
                // バイナリ形式でシリアライズするためのフォーマッターを生成
                BinaryFormatter formatter = new BinaryFormatter();

                // 自分自身をシリアライズ
                formatter.Serialize(stream, this);

                // メモリーストリームの現在位置を先頭に設定
                stream.Position = 0L;

                // メモリーストリームの内容を逆シリアル化
                return (Membership)formatter.Deserialize(stream);
            }
            finally
            {
                stream.Close();
            }
        }
    }
}
 
それでは会員クラス(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の情報のみ変更できた!

会員1の情報だけが変更された!

 
このように深いコピーはいいことずくめのように見えますが、クラスの内容を一旦ストリームに吐き出し、そのストリームから再度クラスを生成しているので、処理は当然遅くなります。
 
何でもかんでも深いコピーを利用するのではなく、参照系は浅いコピー、更新系は深いコピーを利用するなど、ケースバイケースで使い分けることをおススメします。
g h T
 12,721 Total Views