ICloneableの実装(ディープコピーの場合)は、継承を考えると少々面倒です。シリアライザを利用する方法以外では、派生クラスで必ずCloneメソッドをオーバーライドしなければならないため、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 = 0, int b = 0) {
this.a = a;
this.b = b;
}
protected Base(Base that) { // コピーコンストラクタ
this.a = that.a; // 基底クラスのフィールドをコピー
this.b = that.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 = 0, int b = 0, int c = 0): base(a, b) {
this.c = c;
}
protected Derived(Derived that) : base(that) { // 基底クラスのコピーコンストラクタを呼ぶ
this.c = that.c; // 派生クラスのフィールドをコピー
}
public override object Clone() {
return new Derived(this); // コピーコンストラクタを使ってコピーを作成
}
}
派生クラスで必ずCloneメソッドをオーバーライドしなければなりませんが、実行時のコストも低く、無難な手法です。
リフレクションを使ってインスタンスを作成し、各フィールドをコピーする方法です。リフレクションを利用することで、基底クラスのメソッドで派生クラスのインスタンスを作成できます。
// 基底クラスのCloneメソッド
public virtual object Clone() {
Base instance = (Base)Activator.CreateInstance(GetType());
instance.a = this.a;
instance.b = this.b;
return instance;
}
// 派生クラスのCloneメソッド
public override object Clone() {
Derived instance = (Derived)base.Clone();
instance.c = this.c;
return instance;
}
この方法は、.NET Frameworkのクラスライブラリでも使用されているようですが、いくつか制約があります。まず、派生クラスに引数無しのコンストラクタが必要です(Activator.CreateInstanceメソッドの代わりにObjectクラスのMemberwiseCloneメソッドを使用すれば、この制約は回避できますが...)。また、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);
}
}
}