継承を考慮したICloneableの実装

ここでは、継承した場合でもコピーを作成できるICloneableの実装方法(ディープコピー)をいくつか紹介します。

メモ: 一般に、ディープコピーの実装や保守には手間が掛かります。ここで紹介する実装方法でも、シリアライザを利用する方法以外では、派生クラスで必ずCloneメソッドをオーバーライドしなければなりません。また、クラスのフィールドが増減する度にCloneメソッドを修正する必要があります。可能であればオブジェクトをimmutable(不変)に設計し、ディープコピーの実装を回避した方が良いでしょう。

メモ: ICloneableの仕様では、Cloneメソッドによるコピー処理は、シャローコピー(簡易コピー)とディープコピー(詳細コピー)のどちらでも良いことになっています。その結果、ICloneableを実装していても実装詳細を知らなければCloneメソッドの挙動を予測できません。そのため、公開APIではICloneableを実装しないことが推奨されています。

コピーコンストラクタを利用した実装

基底クラスと派生クラスでコピーコンストラクタを用意し、それらを使ってコピーを作成する方法です。

基底クラスではコピーコンストラクタを用意し、基底クラスのフィールドがコピーされるようにします。Cloneメソッドではコピーコンストラクタを使用して新しいインスタンスを作成します。

// 基底クラス
class Base : ICloneable {
    private int a;
    private int b;

    public int A { get { return this.a; } }
    public int B { get { return this.b; } }

    public Base(int a, int b) {
        this.a = a;
        this.b = b;
    }

    protected Base(Base other) { // コピーコンストラクタ
        this.a = other.a; // 基底クラスのフィールドをコピー
        this.b = other.b;
    }

    public virtual object Clone() {
        return new Base(this); // コピーコンストラクタを使ってコピーを作成
    }
}

派生クラスでもコピーコンストラクタを定義し、それをCloneメソッドから呼び出します。派生クラスのコピーコンストラクタでは基底クラスのコピーコンストラクタを呼びます。

// 派生クラス
class Derived : Base {
    private int c;

    public int C { get { return this.c; } }

    public Derived(int a, int b, int c) : base(a, b) {
        this.c = c;
    }

    protected Derived(Derived other) : base(other) { // 基底クラスのコピーコンストラクタを呼ぶ
        this.c = other.c; // 派生クラスのフィールドをコピー
    }

    public override object Clone() {
        return new Derived(this); // コピーコンストラクタを使ってコピーを作成
    }
}

派生クラスで必ずCloneメソッドをオーバーライドしなければなりませんが、実行時のコストも低く、無難な手法です。

MemberwiseCloneメソッドを利用した実装

ObjectクラスのMemberwiseCloneメソッドを使用して基底クラスのCloneメソッドでインスタンスを作成した後、コピーが必要なフィールドに対してコピーを実行する方法です。MemberwiseCloneメソッドではすべてのフィールドがシャローコピーされたインスタンスが作成されます。そのため、値型のフィールドに対するコピーは不要であり、参照型のフィールドに対してのみコピー処理を記述します。

注意: 値型(構造体型)であっても参照型のフィールドを含む場合には、そのフィールドに対するコピー処理が必要です。また、一般に、参照型であってもStringクラスのようなimmutableオブジェクトであればコピーは不要です。

// 基底クラス
class Base : ICloneable {
    private Other a; // 参照型フィールド
    private int b;

    public Other A { get { return this.a; } }
    public int B { get { return this.b; } }

    public Base(Other a, int b) {
        this.a = a;
        this.b = b;
    }

    public virtual object Clone() {
        Base instance = (Base)this.MemberwiseClone();
        instance.a = (Other)this.a.Clone(); // 参照型フィールドがあればコピーする
        // 値型フィールドのコピーは不要
        return instance;
    }
}
// 派生クラス
class Derived : Base {
    private Other c; // 参照型フィールド

    public Other C { get { return this.c; } }

    public Derived(Other a, int b, Other c) : base(a, b) {
        this.c = c;
    }

    public override object Clone() {
        Derived instance = (Derived)base.Clone(); // 基底クラスのCloneメソッドを呼ぶ
        instance.c = (Other)this.c.Clone(); // 参照型フィールドがあればコピーする
        // 値型フィールドのコピーは不要
        return instance;
    }
}

この方法では、値型に対するコピー処理の記述を省略できます(派生クラスが値型のフィールドしか持たない場合はCloneメソッドのオーバーライドも不要です)。ただし、参照型のフィールドをコピーするために代入が必要となるため、readonlyな参照型のフィールドをコピーできません。

シリアライザを利用した実装

シリアライザ(BinaryFormatter)を用いてインスタンスをコピーする方法です。コピー対象オブジェクトがBinaryFormatterでシリアライズ可能である必要はありますが、基底クラスで実装すれば派生クラスでの対応は必要ありません。簡単な実装でディープコピーを実現できます。欠点は、実行時のコストが高いことです。

// シリアライザによるのCloneメソッド
public object Clone() {
    BinaryFormatter binaryFormatter = new BinaryFormatter();
    using (MemoryStream stream = new MemoryStream()) {
        binaryFormatter.Serialize(stream, this);
        stream.Seek(0, SeekOrigin.Begin);
        return binaryFormatter.Deserialize(stream);
    }
}

おまけ: シリアライザを用いた汎用コピーメソッド

シリアライザを用いたコピーは、拡張メソッドにすることで、どのオブジェクトに対しても実行できます(ただし、コピー対象オブジェクトがシリアライズ可能である必要があります)。

static class Extensions {
    public static T Copy<T>(this T target) {
        BinaryFormatter binaryFormatter = new BinaryFormatter();
        using (MemoryStream stream = new MemoryStream()) {
            binaryFormatter.Serialize(stream, target);
            stream.Seek(0, SeekOrigin.Begin);
            return (T)binaryFormatter.Deserialize(stream);
        }
    }
}