ひとくちに複製と言っても、単純にコピー元の値をコピー先に代入することで行う複製や、なんらかのメソッドを呼び出すことで行う複製などさまざまな方法があります。 また複製を行う機能を実装する場合も、構造体やクラスなど複数のフィールドからなる型ではすべてのフィールドを複製する必要があります。 これらの他の型を内包する複合型の場合では、内包するオブジェクトも含めて複製するか、参照のみをコピーするかといったことが考えられます。 さらに、.NET Frameworkでは型には値型と参照型の二種類が存在するため、複製の際にはそれらの違いも考慮する必要があります。
ここではオブジェクトの複製を作成する方法と、複製を作成する機能を提供するICloneableインターフェイスについて解説します。
オブジェクトの複製方法
代入による複製
もっとも簡単にオブジェクトのコピーを作成する方法として、値の代入によって代入先をコピーとする方法があります。
このような代入によるコピーは値型のみで行えます。 値型(構造体・列挙体)では、代入によって代入元とビット単位で同じ値が複製され、代入先へと設定されます。 代入元と代入先の実体はそれぞれ異なるものであるため、代入元のインスタンスのメンバに対する変更は、代入先のインスタンスには影響しません。
一方参照型(クラスなど)では、代入元のインスタンスへの参照が代入先に設定されます。 代入によって代入元と代入先の変数が参照する実体は同一のインスタンスとなるため、代入元のインスタンスのメンバに対する変更は、同じインスタンスを参照している代入先にも反映されます。 このように、参照型の代入ではインスタンスのコピーは作成されません。 これは、参照型では代入によって実体ではなく参照がコピーされると見ることもできます。
なお、文字列型(string)は参照型ですが、代入時の挙動は一見すると値型と同様になります。 この点について詳しくは文字列とStringクラス §.参照型としての挙動で解説しています。
その他値型と参照型の違いについて詳しくは値型と参照型を参照してください。
コピーコンストラクタによる複製
値型・参照型のどちらでも使える方法として、型にコピーコンストラクタを用意する方法があります。
コピーコンストラクタでは、複製したいインスタンスを引数にとり、そのインスタンスの各フィールドの値を自インスタンスのフィールドにコピーすることにより、与えられたインスタンスと同等のインスタンス(つまり複製)を構築します。 この方法には、参照型でも適用できるほか、必要に応じてコピーの際の動作も任意に定義できるという利点があります。
コピー元とコピー先のインスタンスは異なるものなので、当然コピー元に変更を加えてもコピー先には影響しません。 ただこの方法では、コピーすべきフィールドが多数ある型の場合は、コピーコンストラクタの記述が面倒になるという欠点もあります。
Object.MemberwiseCloneによる複製
Objectクラスより継承されるMemberwiseCloneメソッドを用いることで、すべてのフィールドの値がコピーされたオブジェクトの複製を作成することができます。 コピーコンストラクタによる複製ではコピーすべきフィールドの数だけコピー処理を記述する必要がありますが、このメソッドではコピーすべきフィールドが多数ある場合でもメソッド呼び出し一回でオブジェクトの複製を作成することが出来ます。
MemberwiseCloneメソッドはオブジェクトの簡易コピー(後述)を作成します。 パブリックではなくプロテクトなメソッドのため、型の外部から直接呼び出すことは出来ません。 また、このメソッドの戻り値はObject型となっているので、複製されたインスタンスはキャストしてから使う必要があります。
MemberwiseCloneメソッドが返すオブジェクトはコピー元とは異なるインスタンスなので、当然コピー元に変更を加えてもコピー先には影響しません。 なお、MemberwiseCloneメソッドはオーバーライドすることはできないため、複製時の動作をカスタマイズすることは出来ません。 また当然ながら、コピーされるのはインスタンスメンバのみです。
簡易コピーと詳細コピー
MemberwiseCloneメソッドはオブジェクトの簡易コピーを作成します。 コピーの種類には簡易コピーの他に詳細コピーのふたつがあり、両者の違いは次のようになります。
簡易コピー (浅いコピー、shallow copy) |
詳細コピー (深いコピー、deep copy) |
|
---|---|---|
値型のフィールド | ビット単位でのコピー | |
参照型のフィールド | 参照のみのコピー (同一のインスタンスを参照) |
参照先のインスタンスをコピー (インスタンスの複製を作成) |
MemberwiseCloneメソッドでは簡易コピー、すなわち参照型のフィールドでは参照のみがコピーされるようになります。 この違いを明確に示す例を挙げてみます。 以下の例において、コピーを行うクラスには配列、つまり参照型のフィールドが含まれています。
この結果に表れているとおり、MemberwiseCloneメソッドによって簡易コピーされたcopy.ContactAddressesフィールドはoriginal.ContactAddressesフィールドと同じ配列を参照するため、コピー元に変更を加えるとコピー先にも影響します。
このように、MemberwiseCloneメソッドでは詳細コピーを行うことは出来ないため、詳細コピーを行うにはまずMemberwiseCloneメソッドによって簡易コピーを作成し、その後必要に応じて参照型フィールドの複製することによって詳細コピーとなるようにする必要があります。 以下の例は、先の例を詳細コピーを行うように書き換えたものです。
なお、この例で配列の複製に使用しているArray.Cloneメソッドも配列の簡易コピーを行うメソッドです。 オブジェクトの完全な詳細コピーを作成するには、コピーしようとするフィールドを再帰的に複製していくことが必要になる場合があります。
Array.Cloneメソッドと配列の複製については配列操作 §.複製 (Clone)で解説しています。
シリアライズによる複製
詳細コピーを行う方法の一つとして、シリアライズを利用する方法があります。 例として、シリアライズ方法のひとつであるバイナリシリアル化を用いて詳細コピーを行う例を挙げます。
この例で使用しているBinaryFormatter.Serialize/Deserializeについて、.NET 5以降においては使用は推奨されず、できるだけ早く使用をやめる必要があるとされています。 以下のコードでは、これを示すコンパイル時警告SYSLIB0011が出力されます。 特にASP.NET 5.0以降では、明示的にBinaryFormatterの使用を有効にしない限り常に例外NotSupportedExceptionがスローされます。
BinaryFormatter 型は危険であり、データ処理用としては "推奨されません"。 アプリケーションでは、処理するデータが信頼できると思われる場合でも、できるだけ早く BinaryFormatter の使用をやめる必要があります。 BinaryFormatter は安全ではなく、セキュリティで保護することはできません。
BinaryFormatter セキュリティ ガイド | Microsoft Docs
BinaryFormatter シリアル化メソッドが古い形式になり、ASP.NET アプリでは使用不可に
BinaryFormatter、Formatter、および IFormatter の Serialize と Deserialize のメソッドが古いと見なされ、警告が示されるようになりました。 また、ASP.NET アプリでは、BinaryFormatter のシリアル化が既定で禁止されます。
変更の説明
BinaryFormatter のセキュリティ脆弱性により、次のメソッドは古いと見なされ、ID SYSLIB0011 のコンパイル時警告が生成されるようになりました。 また、ASP.NET Core 5.0 以降のアプリでは、Web アプリによって BinaryFormatter 機能が再有効化されていない限り、NotSupportedException がスローされます。
基本クラス ライブラリの破壊的変更 - .NET Core | Microsoft Docs
- BinaryFormatter.Serialize
- BinaryFormatter.Deserialize
これに従い、(特にBinaryFormatter/SoapFormatterを使用した)シリアライズによるオブジェクトの複製は、ASP.NET 5.0以降では明示的に有効にしない限り使用できない手段で、またそれ以外のフレームワークでも推奨できる手段ではなくなっています。
オブジェクトの詳細コピーを行いたい場合は、コピーコンストラクタやMemberwiseCloneメソッド、またそれらによる複製を実装・提供するICloneableインターフェイス・複製用のメソッドを組み合わせることにより、オブジェクト内における複製が必要なフィールドを再帰的に詳細コピーするのが望ましい実装となります。
この方法の利点は、詳細コピー時の動作をシリアライズ属性などにより定義できる点です。 一方、フィールドの型がシリアライズをサポートしていない型の場合は複製できないため個別に処理すり必要があるという欠点もあります。
シリアライズの詳細についてはシリアライズ、特にシリアライズ動作の制御についてはBinaryFormatter・SoapFormatterで解説しています。
また、この方法ではコピーコンストラクタやMemberwiseCloneメソッドを使った方法とは異なり、複製処理を複製対象のクラス外に記述することができる点も利点の1つです。
次の例にあるCloneObjectメソッドは、引数で与えられた任意のオブジェクトの複製を作成して返します。 複製対象がシリアライズ可能である限り、どのような型のオブジェクトも複製することができます。
構造体の複写
memcopy
・CopyMemory
のようにポインタを介してメモリ上にある内容を構造体にコピー(複写)するといった方法や、構造体のバイト表現を取得してバイト配列へ複写する・バイト配列から構造体に複写する、といった方法についてはBinaryReader・BinaryWriterでの構造体の読み書きで解説しています。
ICloneableインターフェイス
ICloneableインターフェイスはオブジェクトが複製可能であることを表すインターフェイスです。 このインターフェイスを実装することによって、型が複製を作成する能力を持つということを明示することが出来ます。 また、ICloneableインターフェイスには唯一のメソッドCloneが用意されていて、このメソッドを適切に実装することでオブジェクトの複製を作成する機能を提供することが出来ます。
ICloneableによる複製機能の公開
以下はICloneableインターフェイスを用いてクラスに複製機能を持たせた例です。 複製を作成するためにMemberwiseCloneメソッドを用いています。
上記の例を見ても分かるとおり、ICloneable.Cloneメソッドの戻り値はObject型なので呼び出し側でのキャストが必要になります。
次の例のように、ICloneable.CloneメソッドをC#では明示的な実装、VBではプライベートなメソッドにして隠蔽し、複製処理の実装は戻り値は型定義したメソッドで行うようにすることで、呼び出し側でのキャストが不要になります。
実装時に留意すべき点など
ICloneable.Cloneメソッドでは、戻り値が複製されるオブジェクトと同じ型であることは求められていますが、複製の際に簡易コピーと詳細コピーのどちらが行われるかは実装次第となっています。 そのためICloneableを実装していても、Cloneメソッドで簡易コピーと詳細コピーのどちらが作成されるかは型次第となります。 実際には、ほとんどの場合は簡易コピーとなっています。 ただ、ICloneableインターフェイスはあくまで複製を作成する機能を提供するだけで、どのような方法で複製するかは実装次第という点に留意する必要があります。
ICloneableインターフェイスを実装しつつ、簡易コピーと詳細コピーの両方をサポートしたい場合は、その実例としてXmlNodeクラスのCloneメソッドとCloneNodeメソッドを参考とすることができます。 XmlNode.CloneNodeメソッドでは引数で簡易コピーと詳細コピーのどちらを行うかを指定することができ、XmlNode.Cloneメソッドでは単に簡易コピーを行うようになっています。
このように、
- ICloneable.Cloneメソッドでは簡易コピーを返す
- 詳細コピーによって複製を行う、あるいは詳細コピーか簡易コピーを選択して複製を行うメソッドはICloneable.Cloneとは個別に用意する
というのが簡易コピーと詳細コピーの両方をサポートする際によく採られる手法と思われます。
また、ICloneableインターフェイスにはICloneable<T>といったジェネリックなバージョンは用意されていません。 戻り値の型はObjectであるため、誤って複製元と互換性のない型を返していてもコンパイル時には検出できない点にも注意が必要です。