連載
» 2017年05月24日 05時00分 公開

.NET TIPS:オブジェクトや配列などの複製を作るには?(ディープコピー編)[C#/VB]

配列やオブジェクトを複製する際にディープコピーを行うには、BinaryFormatterクラスやサードパーティー製のシリアライザーを使用してシリアライズ/デシリアライズするとよい。

[山本康彦,BluewaterSoft/Microsoft MVP for Windows Development]
「.NET TIPS」のインデックス

連載目次

 オブジェクトの複製を作るCloneメソッドは、いくつものクラスに実装されている(System名前空間のICloneableインタフェースを実装しているもの)。しかし、「.NET TIPS:配列の複製を作るには?(シャローコピー編)[C#/VB]」で解説したように、Cloneメソッドはシャローコピーである(参照だけを複製する)。また、Cloneメソッドを持たないクラスも多い。

 では、ディープコピー(参照先のオブジェクトも複製)するにはどうしたらよいだろうか? コピー対象になるオブジェクトの全てがシリアライズ可能であれば、簡単に実現できる。本稿ではその方法を解説する。

 なお、本稿で主に扱うBinaryFormatterクラス(System.Runtime.Serialization.Formatters.Binary.名前空間)は.NET Frameworkの最初からあるものだが、本稿はそれ以降の内容も含んでいる。サンプルコードをそのまま試すには、Visual Studio 2015(またはそれ以降)が必要である。

ディープコピーするには?

 コピー対象になる全てのオブジェクトがシリアライズ可能であれば、シリアライズしてからデシリアライズすればよい。

 具体的には、次のコードに示すDeepCloneメソッドのような実装をする。ここでは、プライベートなメンバ変数などもシリアライズできるBinaryFormatterクラスを使い、拡張メソッドにしている。なお、ASP.NET CoreやUWP/Xamarinなどでは方法が異なる(後述)。

public static class ObjectExtension
{
  // ディープコピーの複製を作る拡張メソッド
  public static T DeepClone<T>(this T src)
  {
    using (var memoryStream = new System.IO.MemoryStream())
    {
      var binaryFormatter 
        = new System.Runtime.Serialization
              .Formatters.Binary.BinaryFormatter();
      binaryFormatter.Serialize(memoryStream, src); // シリアライズ
      memoryStream.Seek(0, System.IO.SeekOrigin.Begin);
      return (T)binaryFormatter.Deserialize(memoryStream); // デシリアライズ
    }
  }
}

Public Module ObjectExtension

  ' ディープコピーの複製を作る拡張メソッド
  <Runtime.CompilerServices.Extension()>
  Public Function DeepClone(Of T)(src As T) As T
    Using memoryStream = New System.IO.MemoryStream()
      Dim binaryFormatter _
        = New System.Runtime.Serialization _
              .Formatters.Binary.BinaryFormatter()
      binaryFormatter.Serialize(memoryStream, src) ' シリアライズ
      memoryStream.Seek(0, System.IO.SeekOrigin.Begin)
      Return binaryFormatter.Deserialize(memoryStream) ' デシリアライズ
    End Using
  End Function

End Module

ディープコピーを行う拡張メソッドの例(上:C#、下:VB)
BinaryFormatterクラスは、参照先のオブジェクトも含めてシリアライズする。また、プライベートなメンバ変数やプロパティであっても、シリアライズしてくれる。
このようにメモリ上にシリアライズしたものをデシリアライズすれば、元のオブジェクトとは独立した別のものとして復元される。すなわち、参照先も含めてディープコピーしたことになるのだ。

実際の例

 上記のDeepCloneメソッドを使うコンソールアプリの例を次のコードに示す。「.NET TIPS:配列の複製を作るには?(シャローコピー編)[C#/VB]」の「シャローコピーであることを確かめる」に掲載したコードと見比べてほしい。

 この方法で、配列だけでなく、Cloneメソッドを持たないList<T>クラスなどのジェネリックコレクションもディープコピーできる。

using System;
using System.Collections.Generic;
using System.Linq;
using static System.Console;

// 配列に格納するクラス
[Serializable]
public class SampleData
{
  public int Index { get; set; }
  public string Name { get; set; }
  public override string ToString() => $"{Index:00}:{Name}";
}

class Program
{
  static void Main(string[] args)
  {
    SampleData[] src = {
      new SampleData {Index=1, Name="aaa" },
      new SampleData {Index=2, Name="bbb" },
      new SampleData {Index=3, Name="ccc" },
    };

    // ディープコピー
    var clone = src.DeepClone();
    WriteLine($"複製先:{string.Join(", ", clone.Select(s => s.ToString()))}");
    // 出力:
    // 複製先:01:aaa, 02:bbb, 03:ccc

    // 複製元のオブジェクトに変更を加える
    src[1].Name = "ZZZ";
    WriteLine($"複製元:{string.Join(", ", src.Select(s => s.ToString()))}");
    // 出力:
    // 複製元:01:aaa, 02:ZZZ, 03:ccc

    // しかし、複製先は変わっていない
    WriteLine($"複製先:{string.Join(", ", clone.Select(s => s.ToString()))}");
    // 出力:
    // 複製先:01:aaa, 02:bbb, 03:ccc

#if DEBUG
    ReadKey();
#endif
  }
}

Imports System.Console

' 配列に格納するクラス
<Serializable>
Public Class SampleData
  Public Property Index As Integer
  Public Property Name As String
  Public Overrides Function ToString() As String
    Return $"{Index:00}:{Name}"
  End Function
End Class

Module Module1
  Sub Main()
    Dim src() As SampleData = {
        New SampleData With {.Index = 1, .Name = "aaa"},
        New SampleData With {.Index = 2, .Name = "bbb"},
        New SampleData With {.Index = 3, .Name = "ccc"}
      }

    ' ディープコピー
    Dim clone = src.DeepClone()
    WriteLine($"複製先:{String.Join(", ", clone.Select(Function(s) s.ToString()))}")
    ' 出力:
    ' 複製先:01:aaa, 02:bbb, 03:ccc

    ' 複製元のオブジェクトに変更を加える
    src(1).Name = "ZZZ"
    WriteLine($"複製元:{String.Join(", ", src.Select(Function(s) s.ToString()))}")
    ' 出力:
    ' 複製元:01:aaa, 02:ZZZ, 03:ccc

    'しかし、複製先は変わっていない
    WriteLine($"複製先:{String.Join(", ", clone.Select(Function(s) s.ToString()))}")
    ' 出力:
    ' 複製先:01:aaa, 02:bbb, 03:ccc

#If DEBUG Then
    ReadKey()
#End If
  End Sub
End Module

ディープコピーで配列を複製するコンソールアプリの例(上:C#、下:VB)
.NET TIPS:配列の複製を作るには?(シャローコピー編)[C#/VB]」掲載のシャローコピーのサンプルコードとの主な違いは、次の2点だ。
・ シリアライズ可能にするため、SampleDataクラスにSerializable属性を付けた。
・ Cloneメソッドの呼び出しを、DeepCloneメソッドの呼び出しに変更した。
なお、DeepCloneメソッドを定義した名前空間と異なる名前空間にあるコードから呼び出すときは、名前空間のインポートが必要である。

.NET Coreの場合

 .NET Core 1.xにはBinaryFormatterクラスがない。そこで、サードパーティー製のシリアライザーを使うとよい。

 次のコードに、MsgPack.Cliを使ったDeepClone拡張メソッドの例を示す(C#のみ)。

 MsgPack.Cliは比較的古くからあるシリアライザーで、PCLやXamarinのプロジェクトにはNuGetから導入できる。ただし、UWPのプロジェクトにはソースコードを組み込む必要があってちょっと使いにくい。現在では、さらに高速で使いやすいものも出てきているので(例えば@neuecc氏によるMessagePack for C#など)、いろいろと試してみてほしい。

public static class ObjectExtension
{
  // ディープコピーの複製を作る拡張メソッド
  // MessagePack for CLIを利用
  // https://www.nuget.org/packages/MsgPack.Cli/
  public static T DeepClone<T>(this T src)
  {
    using (var memoryStream = new System.IO.MemoryStream())
    {
      var formatter = MsgPack.Serialization.MessagePackSerializer.Get<T>();
      formatter.Pack(memoryStream, src); // シリアライズ
      memoryStream.Seek(0, System.IO.SeekOrigin.Begin);
      return formatter.Unpack(memoryStream); // デシリアライズ
    }
  }
}

.NET Core用のDeepClone拡張メソッドの例(C#)

 なお、.NET Core 1.xにはSerializable属性もないので、シリアライズ対象のクラスにはSystem.Runtime.Serialization名前空間のDataContract属性とDataMember属性を使う(次のコード)。

// 配列に格納するクラス
[System.Runtime.Serialization.DataContract]
public class SampleData
{
  [System.Runtime.Serialization.DataMember]
  public int Index { get; set; }

  [System.Runtime.Serialization.DataMember]
  public string Name { get; set; }

  public override string ToString() => $"{Index:00}:{Name}";
}

.NET Core用のSampleDataクラスの例(C#)

 .NET Core用のDeepClone拡張メソッドの呼び出し方は、前述した.NET Frameworkのものと同じだ。コードは割愛するが、SampleDataクラスの配列をディープコピーするUWPアプリの例を次の画像に示しておく。

ディープコピーを行うUWPアプリの例 ディープコピーを行うUWPアプリの例
前述の.NET Framework用のコードでWriteLineメソッドを使ってコンソールに出力していたところを、UWPアプリのTextBoxコントロールに書き出すようにしたもの。

まとめ

 シリアライズ可能なオブジェクトであれば、BinaryFormatterクラスを利用してディープコピーできる。また、BinaryFormatterクラスのない.NET Coreでは、サードパーティー製のシリアライザーを使うとよい。

利用可能バージョン:.NET Framework 1.0以降(サンプルコードにはそれ以降の機能/構文も含む)
カテゴリ:クラス・ライブラリ 処理対象:オブジェクト
使用ライブラリ:BinaryFormatterクラス(System.Runtime.Serialization.Formatters名前空間)
関連TIPS:配列の複製を作るには?(シャローコピー編)[C#/VB]
関連TIPS:C#で配列を宣言するには?
関連TIPS:VB.NETで配列を宣言するには?
関連TIPS:文字列配列内の文字列を連結するには?
関連TIPS:構文:クラス名を書かずに静的メソッドを呼び出すには?[C# 6.0]
関連TIPS:VB.NETでクラス名を省略してメソッドや定数を利用するには?
関連TIPS:数値を右詰めや0埋めで文字列化するには?[C#、VB]
関連TIPS:Visual Studioでコンソール・アプリケーションのデバッグ実行時にコマンド・プロンプトを閉じないようにするには?


「.NET TIPS」のインデックス

.NET TIPS

Copyright© 1999-2017 Digital Advantage Corp. All Rights Reserved.

@IT Special

- PR -

TechTargetジャパン

この記事に関連するホワイトペーパー

RSSについて

アイティメディアIDについて

メールマガジン登録

@ITのメールマガジンは、 もちろん、すべて無料です。ぜひメールマガジンをご購読ください。