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

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

オブジェクトの複製方法

代入による複製

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

代入による値型の複製
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);
  }
}
代入による値型の複製
Imports System

Structure ValueType
  Public ID As Integer
  Public Name As String
End Structure

Class Sample
  Shared Sub Main()
    Dim original, copy As ValueType

    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)
  End Sub
End Class
実行結果
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);
  }
}
代入元への変更と代入先での変化 (値型の場合)
Imports System

' 値型
Structure ValueType
  Public ID As Integer
  Public Name As String
End Structure

Class Sample
  Shared Sub Main()
    Dim original, copy As ValueType


    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)
  End Sub
End Class
実行結果
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);
  }
}
代入元への変更と代入先での変化 (参照型の場合)
Imports System

' 参照型
Class ReferenceType
  Public ID As Integer
  Public Name As String
End Class

Class Sample
  Shared Sub Main()
    Dim original, copy As ReferenceType

    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)
  End Sub
End Class
実行結果
original = 2:Alice
copy     = 2:Alice
original = 2:Bob
copy     = 2:Bob

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

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

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

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

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

コピーコンストラクタによる複製
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);
  }
}
コピーコンストラクタによる複製
Imports System

Class Account
  Public ID As Integer
  Public Name As String

  ' デフォルトコンストラクタ
  Public Sub New()
  End Sub

  ' コピーコンストラクタ
  Public Sub New(ByVal source As Account)
    MyClass.ID   = source.ID
    MyClass.Name = source.Name
  End Sub
End Class

Class Sample
  Shared Sub Main()
    Dim original, copy As Account

    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)
  End Sub
End Class
実行結果
original = 2:Alice
copy     = 2:Alice
original = 3:Bob
copy     = 2:Alice

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

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);
  }
}
Object.MemberwiseCloneメソッドによる複製
Imports System

Class Account
  Public ID As Integer
  Public Name As String

  ' コピーを作成するメソッド
  Public Function Clone() As Account
    Return CType(MemberwiseClone(), Account)
  End Function
End Class

Class Sample
  Shared Sub Main()
    Dim original, copy As Account

    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)
  End Sub
End Class
実行結果
original = 2:Alice
copy     = 2:Alice
original = 3:Bob
copy     = 2:Alice

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

簡易コピーと詳細コピー

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));
  }
}
簡易コピー後にコピー元の参照型フィールドを変更した場合
Imports System

Class Account
  Public ID As Integer
  Public Name As String
  Public ContactAddresses() As String ' 配列(参照型)フィールド

  Public Function Clone() As Account
    ' 簡易コピーを作成して返す
    Return CType(MemberwiseClone(), Account)
  End Function
End Class

Class Sample
  Shared Sub Main()
    Dim original, copy As Account

    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))
  End Sub
End Class
実行結果
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));
  }
}
詳細コピー後にコピー元の参照型フィールドを変更した場合
Imports System

Class Account
  Public ID As Integer
  Public Name As String
  Public ContactAddresses() As String

  Public Function Clone() As Account
    ' 簡易コピーを作成する
    Dim cloned As Account = CType(MemberwiseClone(), Account)

    ' 参照型フィールドの複製を作成する(詳細コピーを行う)
    If Not MyClass.ContactAddresses Is Nothing Then
      cloned.ContactAddresses = CType(MyClass.ContactAddresses.Clone(), String())
    End If

    Return cloned
  End Function
End Class

Class Sample
  Shared Sub Main()
    Dim original, copy As Account

    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))
  End Sub
End Class
実行結果
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)で解説しています。

シリアライズによる複製

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

この例で使用している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 がスローされます。

  • BinaryFormatter.Serialize
  • BinaryFormatter.Deserialize
基本クラス ライブラリの破壊的変更 - .NET Core | Microsoft Docs

これに従い、(特にBinaryFormatter/SoapFormatterを使用した)シリアライズによるオブジェクトの複製は、ASP.NET 5.0以降では明示的に有効にしない限り使用できない手段で、またそれ以外のフレームワークでも推奨できる手段ではなくなっています。

オブジェクトの詳細コピーを行いたい場合は、コピーコンストラクタMemberwiseCloneメソッド、またそれらによる複製を実装・提供するICloneableインターフェイス・複製用のメソッドを組み合わせることにより、オブジェクト内における複製が必要なフィールドを再帰的に詳細コピーするのが望ましい実装となります。

シリアライズによる複製
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); // [.NET 5] warning SYSLIB0011: 'BinaryFormatter.Serialize(Stream, object)' は旧形式です ('BinaryFormatter serialization is obsolete and should not be used. See https://aka.ms/binaryformatter for more information.')

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

      // MemoryStreamに格納された内容を逆シリアル化する
      return (Account)f.Deserialize(stream); // [.NET 5] warning SYSLIB0011: 'BinaryFormatter.Deserialize(Stream)' は旧形式です ('BinaryFormatter serialization is obsolete and should not be used. See https://aka.ms/binaryformatter for more information.')
    }
  }
}

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));
  }
}
シリアライズによる複製
Imports System
Imports System.IO
Imports System.Runtime.Serialization
Imports System.Runtime.Serialization.Formatters.Binary

<Serializable> _ ' クラスをシリアル化可能にする
Class Account
  Public ID As Integer
  Public Name As String
  Public ContactAddresses() As String

  Public Function Clone() As Account
    ' シリアル化した内容を保持しておくためのMemoryStreamを作成
    Using stream As New MemoryStream()
      ' バイナリシリアル化を行うためのフォーマッタを作成
      Dim f As New BinaryFormatter()

      ' 現在のインスタンスをシリアル化してMemoryStreamに格納
      f.Serialize(stream, Me) ' [.NET 5] warning SYSLIB0011: 'Public Overloads Sub Serialize(serializationStream As Stream, graph As Object)' は廃止されています: 'BinaryFormatter serialization is obsolete and should not be used. See https://aka.ms/binaryformatter for more information.'。

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

      ' MemoryStreamに格納された内容を逆シリアル化する
      Return CType(f.Deserialize(stream), Account) ' [.NET 5] warning SYSLIB0011: 'Public Overloads Function Deserialize(serializationStream As Stream) As Object' は廃止されています: 'BinaryFormatter serialization is obsolete and should not be used. See https://aka.ms/binaryformatter for more information.'。
    End Using
  End Function
End Class

Class Sample
  Shared Sub Main()
    Dim original, copy As Account

    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))
  End Sub
End Class
実行結果
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); // [.NET 5] warning SYSLIB0011: 'BinaryFormatter.Serialize(Stream, object)' は旧形式です ('BinaryFormatter serialization is obsolete and should not be used. See https://aka.ms/binaryformatter for more information.')

      stream.Position = 0L;

      return (T)f.Deserialize(stream); // [.NET 5] warning SYSLIB0011: 'BinaryFormatter.Deserialize(Stream)' は旧形式です ('BinaryFormatter serialization is obsolete and should not be used. See https://aka.ms/binaryformatter for more information.')
    }
  }

  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));
  }
}
シリアライズを使ってシリアライズ可能な任意のオブジェクトを複製する
Imports System
Imports System.IO
Imports System.Runtime.Serialization
Imports System.Runtime.Serialization.Formatters.Binary

' シリアル化可能なクラス
<Serializable> _ 
Class Account
  Public ID As Integer
  Public Name As String
  Public ContactAddresses() As String
End Class

Class Sample
  ''' <summary>バイナリシリアライズを使って任意の型Tのオブジェクトを複製する</summary>
  ''' <returns><paramref name="source"/>を複製したオブジェクト</returns>
  ''' <exception cref="SerializationException"><paramref name="source"/>がシリアル化可能としてマークされていない</exception>
  Shared Function CloneObject(Of T)(ByVal source As T) As T
    Using stream As New MemoryStream()
      Dim f As New BinaryFormatter()

      f.Serialize(stream, source) ' [.NET 5] warning SYSLIB0011: 'Public Overloads Sub Serialize(serializationStream As Stream, graph As Object)' は廃止されています: 'BinaryFormatter serialization is obsolete and should not be used. See https://aka.ms/binaryformatter for more information.'。

      stream.Position = 0L

      Return CType(f.Deserialize(stream), T) ' [.NET 5] warning SYSLIB0011: 'Public Overloads Function Deserialize(serializationStream As Stream) As Object' は廃止されています: 'BinaryFormatter serialization is obsolete and should not be used. See https://aka.ms/binaryformatter for more information.'。
    End Using
  End Function

  Shared Sub Main()
    ' 数値型の複製
    Dim i1 As Integer = 16
    Dim i2 As Integer = CloneObject(i1)

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

    ' 文字列型の複製
    Dim s1 As String = "Hello, world!"
    Dim s2 As String = CloneObject(s1)

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

    ' 独自に作成したクラスの複製
    Dim a1 As New Account()

    a1.ID = 2
    a1.Name = "Alice"
    a1.ContactAddresses = New String() {"alice@example.com", "http://example.net/~alice/"}

    Dim a2 As Account = 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))
  End Sub
End Class
実行結果
16
16
Hello, world!
Hello, world!
2:Alice (bob@example.com, http://example.net/~alice/)
2:Alice (alice@example.com, http://example.net/~alice/)

構造体の複写

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

ICloneableインターフェイス

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

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);
  }
}
ICloneableインターフェイスを実装して複製機能を公開する
Imports System

Class Account
  Implements ICloneable

  Public ID As Integer
  Public Name As String

  ' ICloneable.Cloneの実装
  Public Function Clone() As Object Implements ICloneable.Clone
    Return MemberwiseClone()
  End Function
End Class

Class Sample
  Shared Sub Main()
    Dim original, copy As Account

    original = New Account()
    original.ID = 2
    original.Name = "Alice"

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

    copy = CType(original.Clone(), Account) ' 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)
  End Sub
End Class
実行結果
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);
  }
}
ICloneable.Cloneの実装を隠蔽してタイプセーフな複製メソッドのみを公開する
Imports System

Class Account
  Implements ICloneable

  Public ID As Integer
  Public Name As String

  ' 複製を作成するメソッド
  Public Function Clone() As Account
    Return CType(MemberwiseClone(), Account)
  End Function

  ' ICloneable.Cloneの実装
  Private Function CloneImpl() As Object Implements ICloneable.Clone
    Return Clone()
  End Function
End Class

Class Sample
  Shared Sub Main()
    Dim original, copy As Account

    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)
  End Sub
End Class
実行結果
original = 2:Alice
copy     = 2:Alice
original = 3:Bob
copy     = 2:Alice

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

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

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

このように、

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

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

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