ひとくちに複製と言っても、単純にコピー元の値をコピー先に代入することで行う複製や、なんらかのメソッドを呼び出すことで行う複製などさまざまな方法があります。 また複製を行う機能を実装する場合も、構造体やクラスなど複数のフィールドからなる型ではすべてのフィールドを複製する必要があります。 これらの他の型を内包する複合型の場合では、内包するオブジェクトも含めて複製するか、参照のみをコピーするかといったことが考えられます。 さらに、.NET Frameworkでは型には値型と参照型の二種類が存在するため、複製の際にはそれらの違いも考慮する必要があります。

ここではオブジェクトの複製を作成する方法と、複製を作成する機能を提供するICloneableインターフェイスについて解説します。

§1 オブジェクトの複製方法

§1.1 代入による複製

もっとも簡単にオブジェクトのコピーを作成する方法として、値の代入によって代入先をコピーとする方法があります。

代入による値型の複製
using System;

struct ValueType {
  public int ID;
  public string Name;
}

class Sample {
  static void Main()
  {
    ValueType original, copy;

    original.ID = 2;
    original.Name = "Alice";

    Console.WriteLine("original = {0}:{1}", original.ID, original.Name);

    copy = original; // 代入によるコピー

    Console.WriteLine("copy     = {0}:{1}", copy.ID, copy.Name);
  }
}
実行結果
original = 2:Alice
copy     = 2:Alice

このような代入によるコピーは値型のみで行えます。 値型(構造体・列挙体)では、代入によって代入元とビット単位で同じ値が複製され、代入先へと設定されます。 代入元と代入先の実体はそれぞれ異なるものであるため、代入元のインスタンスのメンバに対する変更は、代入先のインスタンスには影響しません。

一方参照型(クラスなど)では、代入元のインスタンスへの参照が代入先に設定されます。 代入によって代入元と代入先の変数が参照する実体は同一のインスタンスとなるため、代入元のインスタンスのメンバに対する変更は、同じインスタンスを参照している代入先にも反映されます。 このように、参照型の代入ではインスタンスのコピーは作成されません。 これは、参照型では代入によって実体ではなく参照がコピーされると見ることもできます。

代入元への変更と代入先での変化 (値型の場合)
using System;

// 値型
struct ValueType {
  public int ID;
  public string Name;
}

class Sample {
  static void Main()
  {
    ValueType original, copy;


    original.ID = 2;
    original.Name = "Alice";

    Console.WriteLine("original = {0}:{1}", original.ID, original.Name);

    copy = original; // 代入による値のコピー

    Console.WriteLine("copy    = {0}:{1}", copy.ID, copy.Name);

    original.Name = "Bob"; // 代入元のメンバを変更

    Console.WriteLine("original = {0}:{1}", original.ID, original.Name);
    Console.WriteLine("copy    = {0}:{1}", copy.ID, copy.Name);
  }
}
実行結果
original = 2:Alice
copy     = 2:Alice
original = 2:Bob
copy     = 2:Alice
代入元への変更と代入先での変化 (参照型の場合)
using System;

// 参照型
class ReferenceType {
  public int ID;
  public string Name;
}

class Sample {
  static void Main()
  {
    ReferenceType original, copy;

    original = new ReferenceType();
    original.ID = 2;
    original.Name = "Alice";

    Console.WriteLine("original = {0}:{1}", original.ID, original.Name);

    copy = original; // 代入による参照のコピー

    Console.WriteLine("copy     = {0}:{1}", copy.ID, copy.Name);

    original.Name = "Bob"; // 代入元のメンバを変更

    Console.WriteLine("original = {0}:{1}", original.ID, original.Name);
    Console.WriteLine("copy     = {0}:{1}", copy.ID, copy.Name);
  }
}
実行結果
original = 2:Alice
copy     = 2:Alice
original = 2:Bob
copy     = 2:Bob

なお、文字列型(string)は参照型ですが、代入時の挙動は一見すると値型と同様になります。 この点について詳しくは文字列とStringクラス §.参照型としての挙動で解説しています。

その他値型と参照型の違いについて詳しくは値型と参照型を参照してください。

§1.2 コピーコンストラクタによる複製

値型・参照型のどちらでも使える方法として、型にコピーコンストラクタを用意する方法があります。

コピーコンストラクタでは、複製したいインスタンスを引数にとり、そのインスタンスの各フィールドの値を自インスタンスのフィールドにコピーすることにより、与えられたインスタンスと同等のインスタンス(つまり複製)を構築します。 この方法には、参照型でも適用できるほか、必要に応じてコピーの際の動作も任意に定義できるという利点があります。

コピーコンストラクタによる複製
using System;

class Account {
  public int ID;
  public string Name;

  // デフォルトコンストラクタ
  public Account()
  {
  }

  // コピーコンストラクタ
  public Account(Account source)
  {
    this.ID   = source.ID;
    this.Name = source.Name;
  }
}

class Sample {
  static void Main()
  {
    Account original, copy;

    original = new Account();
    original.ID = 2;
    original.Name = "Alice";

    Console.WriteLine("original = {0}:{1}", original.ID, original.Name);

    copy = new Account(original); // コピーコンストラクタを使ってコピー

    Console.WriteLine("copy     = {0}:{1}", copy.ID, copy.Name);

    original.ID = 3; // コピー元のメンバを変更
    original.Name = "Bob";

    Console.WriteLine("original = {0}:{1}", original.ID, original.Name);
    Console.WriteLine("copy     = {0}:{1}", copy.ID, copy.Name);
  }
}
実行結果
original = 2:Alice
copy     = 2:Alice
original = 3:Bob
copy     = 2:Alice

コピー元とコピー先のインスタンスは異なるものなので、当然コピー元に変更を加えてもコピー先には影響しません。 ただこの方法では、コピーすべきフィールドが多数ある型の場合は、コピーコンストラクタの記述が面倒になるという欠点もあります。

§1.3 Object.MemberwiseCloneによる複製

Objectクラスより継承されるMemberwiseCloneメソッドを用いることで、すべてのフィールドの値がコピーされたオブジェクトの複製を作成することができます。 コピーコンストラクタによる複製ではコピーすべきフィールドの数だけコピー処理を記述する必要がありますが、このメソッドではコピーすべきフィールドが多数ある場合でもメソッド呼び出し一回でオブジェクトの複製を作成することが出来ます。

MemberwiseCloneメソッドはオブジェクトの簡易コピー(後述)を作成します。 パブリックではなくプロテクトなメソッドのため、型の外部から直接呼び出すことは出来ません。 また、このメソッドの戻り値はObject型となっているので、複製されたインスタンスはキャストしてから使う必要があります。

Object.MemberwiseCloneメソッドによる複製
using System;

class Account {
  public int ID;
  public string Name;

  // コピーを作成するメソッド
  public Account Clone()
  {
    return (Account)MemberwiseClone();
  }
}

class Sample {
  static void Main()
  {
    Account original, copy;

    original = new Account();
    original.ID = 2;
    original.Name = "Alice";

    Console.WriteLine("original = {0}:{1}", original.ID, original.Name);

    copy = original.Clone(); // MemberwiseCloneを使ってコピー

    Console.WriteLine("copy     = {0}:{1}", copy.ID, copy.Name);

    original.ID = 3; // コピー元のメンバを変更
    original.Name = "Bob";

    Console.WriteLine("original = {0}:{1}", original.ID, original.Name);
    Console.WriteLine("copy     = {0}:{1}", copy.ID, copy.Name);
  }
}
実行結果
original = 2:Alice
copy     = 2:Alice
original = 3:Bob
copy     = 2:Alice

MemberwiseCloneメソッドが返すオブジェクトはコピー元とは異なるインスタンスなので、当然コピー元に変更を加えてもコピー先には影響しません。 なお、MemberwiseCloneメソッドはオーバーライドすることはできないため、複製時の動作をカスタマイズすることは出来ません。 また当然ながら、コピーされるのはインスタンスメンバのみです。

§1.3.1 簡易コピーと詳細コピー

MemberwiseCloneメソッドはオブジェクトの簡易コピーを作成します。 コピーの種類には簡易コピーの他に詳細コピーのふたつがあり、両者の違いは次のようになります。

簡易コピーと詳細コピーの動作
簡易コピー
(浅いコピー、shallow copy)
詳細コピー
(深いコピー、deep copy)
値型のフィールド ビット単位でのコピー
参照型のフィールド 参照のみのコピー
(同一のインスタンスを参照)
参照先のインスタンスをコピー
(インスタンスの複製を作成)

MemberwiseCloneメソッドでは簡易コピー、すなわち参照型のフィールドでは参照のみがコピーされるようになります。 この違いを明確に示す例を挙げてみます。 以下の例において、コピーを行うクラスには配列、つまり参照型のフィールドが含まれています。

簡易コピー後にコピー元の参照型フィールドを変更した場合
using System;

class Account {
  public int ID;
  public string Name;
  public string[] ContactAddresses; // 配列(参照型)フィールド

  public Account Clone()
  {
    // 簡易コピーを作成して返す
    return (Account)MemberwiseClone();
  }
}

class Sample {
  static void Main()
  {
    Account original, copy;

    original = new Account();
    original.ID = 2;
    original.Name = "Alice";
    original.ContactAddresses = new string[] {"alice@example.com", "http://example.net/~alice/"};

    Console.WriteLine("original = {0}:{1} ({2})", original.ID, original.Name, string.Join(", ", original.ContactAddresses));

    copy = original.Clone(); // originalの複製(簡易コピー)を作成

    Console.WriteLine("copy     = {0}:{1} ({2})", copy.ID, copy.Name, string.Join(", ", copy.ContactAddresses));

    original.ContactAddresses[0] = "bob@example.com"; // コピー元の参照型フィールドに変更を加える

    Console.WriteLine("original = {0}:{1} ({2})", original.ID, original.Name, string.Join(", ", original.ContactAddresses));
    Console.WriteLine("copy     = {0}:{1} ({2})", copy.ID, copy.Name, string.Join(", ", copy.ContactAddresses));
  }
}
実行結果
original = 2:Alice (alice@example.com, http://example.net/~alice/)
copy     = 2:Alice (alice@example.com, http://example.net/~alice/)
original = 2:Alice (bob@example.com, http://example.net/~alice/)
copy     = 2:Alice (bob@example.com, http://example.net/~alice/)

この結果に表れているとおり、MemberwiseCloneメソッドによって簡易コピーされたcopy.ContactAddressesフィールドはoriginal.ContactAddressesフィールドと同じ配列を参照するため、コピー元に変更を加えるとコピー先にも影響します。

このように、MemberwiseCloneメソッドでは詳細コピーを行うことは出来ないため、詳細コピーを行うにはまずMemberwiseCloneメソッドによって簡易コピーを作成し、その後必要に応じて参照型フィールドの複製することによって詳細コピーとなるようにする必要があります。 以下の例は、先の例を詳細コピーを行うように書き換えたものです。

詳細コピー後にコピー元の参照型フィールドを変更した場合
using System;

class Account {
  public int ID;
  public string Name;
  public string[] ContactAddresses;

  public Account Clone()
  {
    // 簡易コピーを作成する
    Account cloned = (Account)MemberwiseClone();

    // 参照型フィールドの複製を作成する(詳細コピーを行う)
    if (this.ContactAddresses != null) {
      cloned.ContactAddresses = (string[])this.ContactAddresses.Clone();
    }

    return cloned;
  }
}

class Sample {
  static void Main()
  {
    Account original, copy;

    original = new Account();
    original.ID = 2;
    original.Name = "Alice";
    original.ContactAddresses = new string[] {"alice@example.com", "http://example.net/~alice/"};

    Console.WriteLine("original = {0}:{1} ({2})", original.ID, original.Name, string.Join(", ", original.ContactAddresses));

    copy = original.Clone(); // originalの複製(詳細コピー)を作成

    Console.WriteLine("copy     = {0}:{1} ({2})", copy.ID, copy.Name, string.Join(", ", copy.ContactAddresses));

    original.ContactAddresses[0] = "bob@example.com"; // コピー元の参照型フィールドに変更を加える

    Console.WriteLine("original = {0}:{1} ({2})", original.ID, original.Name, string.Join(", ", original.ContactAddresses));
    Console.WriteLine("copy     = {0}:{1} ({2})", copy.ID, copy.Name, string.Join(", ", copy.ContactAddresses));
  }
}
実行結果
original = 2:Alice (alice@example.com, http://example.net/~alice/)
copy     = 2:Alice (alice@example.com, http://example.net/~alice/)
original = 2:Alice (bob@example.com, http://example.net/~alice/)
copy     = 2:Alice (alice@example.com, http://example.net/~alice/)

なお、この例で配列の複製に使用しているArray.Cloneメソッドも配列の簡易コピーを行うメソッドです。 オブジェクトの完全な詳細コピーを作成するには、コピーしようとするフィールドを再帰的に複製していくことが必要になる場合があります。

Array.Cloneメソッドと配列の複製については配列操作 §.複製 (Clone)で解説しています。



§1.4 シリアライズによる複製

詳細コピーを行う方法の一つとして、シリアライズを利用する方法があります。 例として、シリアライズ方法のひとつであるバイナリシリアル化を用いて詳細コピーを行う例を挙げます。

シリアライズによる複製
using System;
using System.IO;
using System.Runtime.Serialization;
using System.Runtime.Serialization.Formatters.Binary;

[Serializable] // クラスをシリアル化可能にする
class Account {
  public int ID;
  public string Name;
  public string[] ContactAddresses;

  public Account Clone()
  {
    // シリアル化した内容を保持しておくためのMemoryStreamを作成
    using (MemoryStream stream = new MemoryStream()) {
      // バイナリシリアル化を行うためのフォーマッタを作成
      BinaryFormatter f = new BinaryFormatter();

      // 現在のインスタンスをシリアル化してMemoryStreamに格納
      f.Serialize(stream, this);

      // ストリームの位置を先頭に戻す
      stream.Position = 0L;

      // MemoryStreamに格納された内容を逆シリアル化する
      return (Account)f.Deserialize(stream);
    }
  }
}

class Sample {
  static void Main()
  {
    Account original, copy;

    original = new Account();
    original.ID = 2;
    original.Name = "Alice";
    original.ContactAddresses = new string[] {"alice@example.com", "http://example.net/~alice/"};

    Console.WriteLine("original = {0}:{1} ({2})", original.ID, original.Name, string.Join(", ", original.ContactAddresses));

    copy = original.Clone(); // originalの複製(詳細コピー)を作成

    Console.WriteLine("copy     = {0}:{1} ({2})", copy.ID, copy.Name, string.Join(", ", copy.ContactAddresses));

    original.ContactAddresses[0] = "bob@example.com"; // コピー元の参照型フィールドに変更を加える

    Console.WriteLine("original = {0}:{1} ({2})", original.ID, original.Name, string.Join(", ", original.ContactAddresses));
    Console.WriteLine("copy     = {0}:{1} ({2})", copy.ID, copy.Name, string.Join(", ", copy.ContactAddresses));
  }
}
実行結果
original = 2:Alice (alice@example.com, http://example.net/~alice/)
copy     = 2:Alice (alice@example.com, http://example.net/~alice/)
original = 2:Alice (bob@example.com, http://example.net/~alice/)
copy     = 2:Alice (alice@example.com, http://example.net/~alice/)

この方法の利点は、詳細コピー時の動作をシリアライズ属性などにより定義できる点です。 一方、フィールドの型がシリアライズをサポートしていない型の場合は複製できないため個別に処理すり必要があるという欠点もあります。

シリアライズの詳細についてはシリアライズ、特にシリアライズ動作の制御についてはBinaryFormatter・SoapFormatterで解説しています。

また、この方法ではコピーコンストラクタMemberwiseCloneメソッドを使った方法とは異なり、複製処理を複製対象のクラス外に記述することができる点も利点の1つです。

次の例にあるCloneObjectメソッドは、引数で与えられた任意のオブジェクトの複製を作成して返します。 複製対象がシリアライズ可能である限り、どのような型のオブジェクトも複製することができます。

シリアライズを使ってシリアライズ可能な任意のオブジェクトを複製する
using System;
using System.IO;
using System.Runtime.Serialization;
using System.Runtime.Serialization.Formatters.Binary;

[Serializable] // シリアル化可能なクラス
class Account {
  public int ID;
  public string Name;
  public string[] ContactAddresses;
}

class Sample {
  /// <summary>バイナリシリアライズを使って任意の型Tのオブジェクトを複製する</summary>
  /// <returns><paramref name="source"/>を複製したオブジェクト</returns>
  /// <exception cref="SerializationException"><paramref name="source"/>がシリアル化可能としてマークされていない</exception>
  static T CloneObject<T>(T source)
  {
    // バイナリシリアライズによってsourceの複製を作成する
    using (MemoryStream stream = new MemoryStream()) {
      BinaryFormatter f = new BinaryFormatter();

      f.Serialize(stream, source);

      stream.Position = 0L;

      return (T)f.Deserialize(stream);
    }
  }

  static void Main()
  {
    // 数値型の複製
    int i1 = 16;
    int i2 = CloneObject(i1);

    Console.WriteLine(i1);
    Console.WriteLine(i2);

    // 文字列型の複製
    string s1 = "Hello, world!";
    string s2 = CloneObject(s1);

    Console.WriteLine(s1);
    Console.WriteLine(s2);

    // 独自に作成したクラスの複製
    Account a1 = new Account();

    a1.ID = 2;
    a1.Name = "Alice";
    a1.ContactAddresses = new string[] {"alice@example.com", "http://example.net/~alice/"};

    Account a2 = CloneObject(a1);

    a1.ContactAddresses[0] = "bob@example.com"; // コピー元の参照型フィールドに変更を加える

    Console.WriteLine("{0}:{1} ({2})", a1.ID, a1.Name, string.Join(", ", a1.ContactAddresses));
    Console.WriteLine("{0}:{1} ({2})", a2.ID, a2.Name, string.Join(", ", a2.ContactAddresses));
  }
}
実行結果
16
16
Hello, world!
Hello, world!
2:Alice (bob@example.com, http://example.net/~alice/)
2:Alice (alice@example.com, http://example.net/~alice/)

§1.5 構造体の複写

memcopyCopyMemoryのようにポインタを介してメモリ上にある内容を構造体にコピー(複写)するといった方法や、構造体のバイト表現を取得してバイト配列へ複写する・バイト配列から構造体に複写する、といった方法についてはBinaryReader・BinaryWriterでの構造体の読み書きで解説しています。

§2 ICloneableインターフェイス

ICloneableインターフェイスはオブジェクトが複製可能であることを表すインターフェイスです。 このインターフェイスを実装することによって、型が複製を作成する能力を持つということを明示することが出来ます。 また、ICloneableインターフェイスには唯一のメソッドCloneが用意されていて、このメソッドを適切に実装することでオブジェクトの複製を作成する機能を提供することが出来ます。

§2.1 ICloneableによる複製機能の公開

以下はICloneableインターフェイスを用いてクラスに複製機能を持たせた例です。 複製を作成するためにMemberwiseCloneメソッドを用いています。

ICloneableインターフェイスを実装して複製機能を公開する
using System;

class Account : ICloneable {
  public int ID;
  public string Name;

  // ICloneable.Cloneの実装
  public object Clone()
  {
    return MemberwiseClone();
  }
}

class Sample {
  static void Main()
  {
    Account original, copy;

    original = new Account();
    original.ID = 2;
    original.Name = "Alice";

    Console.WriteLine("original = {0}:{1}", original.ID, original.Name);

    copy = (Account)original.Clone(); // ICloneable.Cloneを使ってコピー

    Console.WriteLine("copy     = {0}:{1}", copy.ID, copy.Name);

    original.ID = 3; // コピー元のメンバを変更
    original.Name = "Bob";

    Console.WriteLine("original = {0}:{1}", original.ID, original.Name);
    Console.WriteLine("copy     = {0}:{1}", copy.ID, copy.Name);
  }
}
実行結果
original = 2:Alice
copy     = 2:Alice
original = 3:Bob
copy     = 2:Alice

上記の例を見ても分かるとおり、ICloneable.Cloneメソッドの戻り値はObject型なので呼び出し側でのキャストが必要になります。

次の例のように、ICloneable.CloneメソッドをC#では明示的な実装、VBではプライベートなメソッドにして隠蔽し、複製処理の実装は戻り値は型定義したメソッドで行うようにすることで、呼び出し側でのキャストが不要になります。

ICloneable.Cloneの実装を隠蔽してタイプセーフな複製メソッドのみを公開する
using System;

class Account : ICloneable {
  public int ID;
  public string Name;

  // 複製を作成するメソッド
  public Account Clone()
  {
    return (Account)MemberwiseClone();
  }

  // ICloneable.Cloneの明示的な実装
  object ICloneable.Clone()
  {
    return Clone();
  }
}

class Sample {
  static void Main()
  {
    Account original, copy;

    original = new Account();
    original.ID = 2;
    original.Name = "Alice";

    Console.WriteLine("original = {0}:{1}", original.ID, original.Name);

    copy = original.Clone(); // 複製を作成

    Console.WriteLine("copy     = {0}:{1}", copy.ID, copy.Name);

    original.ID = 3; // コピー元のメンバを変更
    original.Name = "Bob";

    Console.WriteLine("original = {0}:{1}", original.ID, original.Name);
    Console.WriteLine("copy     = {0}:{1}", copy.ID, copy.Name);
  }
}
実行結果
original = 2:Alice
copy     = 2:Alice
original = 3:Bob
copy     = 2:Alice

§2.2 実装時に留意すべき点など

ICloneable.Cloneメソッドでは、戻り値が複製されるオブジェクトと同じ型であることは求められていますが、複製の際に簡易コピーと詳細コピーのどちらが行われるかは実装次第となっています。 そのためICloneableを実装していても、Cloneメソッドで簡易コピーと詳細コピーのどちらが作成されるかは型次第となります。 実際には、ほとんどの場合は簡易コピーとなっています。 ただ、ICloneableインターフェイスはあくまで複製を作成する機能を提供するだけで、どのような方法で複製するかは実装次第という点に留意する必要があります。

ICloneableインターフェイスを実装しつつ、簡易コピーと詳細コピーの両方をサポートしたい場合は、その実例としてXmlNodeクラスCloneメソッドCloneNodeメソッドを参考とすることができます。 XmlNode.CloneNodeメソッドでは引数で簡易コピーと詳細コピーのどちらを行うかを指定することができ、XmlNode.Cloneメソッドでは単に簡易コピーを行うようになっています。

このように、

  1. ICloneable.Cloneメソッドでは簡易コピーを返す
  2. 詳細コピーによって複製を行う、あるいは詳細コピーか簡易コピーを選択して複製を行うメソッドはICloneable.Cloneとは個別に用意する

というのが簡易コピーと詳細コピーの両方をサポートする際によく採られる手法と思われます。

また、ICloneableインターフェイスにはICloneable<T>といったジェネリックなバージョンは用意されていません。 戻り値の型はObjectであるため、誤って複製元と互換性のない型を返していてもコンパイル時には検出できない点にも注意が必要です。