配列やオブジェクトを複製する際にディープコピーを行うには、BinaryFormatterクラスやサードパーティー製のシリアライザーを使用してシリアライズ/デシリアライズするとよい。
オブジェクトの複製を作る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
上記の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
.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 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用のDeepClone拡張メソッドの呼び出し方は、前述した.NET Frameworkのものと同じだ。コードは割愛するが、SampleDataクラスの配列をディープコピーするUWPアプリの例を次の画像に示しておく。
シリアライズ可能なオブジェクトであれば、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でコンソール・アプリケーションのデバッグ実行時にコマンド・プロンプトを閉じないようにするには?
Copyright© Digital Advantage Corp. All Rights Reserved.