継承を考慮した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);
}
}
}