DeepCopy.Expression – 匿名クラスのオブジェクトを複製する

匿名クラスは、その場で一時的に作成される名前の無いクラスです。匿名クラスは一時的な型のオブジェクトを作成したりするのに便利ですが、シリアライズとデシリアライズを利用した一般的なディープコピーの方法が使えません。
DeepCopy.Expression は匿名クラスのディープコピーにも対応しており、Expression Tree という機能を使って、オブジェクトの構造を解析し、新しいインスタンスとして複製出来ます。

対象のクラス

検証用に、以下の2種類の複製対象のクラスを用意しました。

public sealed class ClassA
{
    public DateTime Date { get; set; }

    public override string ToString() =>
        Date.ToString();
}


public sealed class ClassB
{
    public ClassB()
    {
        Datas = new List<int>();
    }

    public List<int> Datas { get;  }

    public override string ToString() =>
        string.Join(",", Datas);
}

DeepCopy.Expression で匿名クラスの複製

ここでは、先ほどのオブジェクトを複数まとめて一度に複製させるという用途で匿名クラスを使用します。
複製の実行プログラムとしては以下のようにしました。

class Program
{
    static void Main(string[] args)
    {
        var a = new ClassA()
        {
            Date = DateTime.Now
        };
        var b = new ClassB();
        b.Datas.AddRange(new [] { 1, 2, 4, 8 });

        var anonymous = new
        {
            A = a,
            B = b
        };

        var cloned = ObjectCloner.Clone(anonymous);

        Console.WriteLine($"A: Value = {cloned.A}, Reference = {anonymous.A == cloned.A}");
        Console.WriteLine($"B: Value = {cloned.B}, Reference = {anonymous.B == cloned.B}");
    }
}

ClassA のインスタンスと ClassB のインスタンスを anonymous にまとめてから anonymous を複製しています。
これを実行すると匿名クラスのインスタンスがディープコピーされ、そこに含まれるAとBも複製出来ていることが確認できます。

A: Value = 2021/04/17 13:54:07, Reference = False
B: Value = 1,2,4,8, Reference = False

まとめ

あまり使用する機会は無いかもしれませんが、DeepCopy.Expression を使えば匿名クラスのオブジェクトもディープコピーすることが可能です。

DeepCopy.Expression – インターフェースや基底クラスを持つオブジェクトの複製する

  • 2023/06/05 内容を修正

DeepCopy.Expression ライブラリは、インターフェースや基底クラスのディープコピーにも対応しています。インターフェースや基底クラスとは、抽象的な概念や共通の機能を定義したものであり、ポリモーフィズムを使うことで、柔軟で再利用性の高いコードを書くことができるよいうになります。
しかし、ポリモーフィズムを使ったオブジェクトをディープコピーする場合には注意が必要です。シリアライズとデシリアライズを使う方法では、具体的なクラスの情報が失われてしまう可能性があります。

DeepCopy.Expression ライブラリでは、この問題を解決するために、インターフェースや基底クラスの型変数に代入されている具体的なクラスの型情報も保持しておきます。これによりインターフェースや基底クラスのディープコピーでもポリモーフィズムが保たれます。

例えば次のようなコードでディープコピーすることができます。

対象のクラス

検証用に以下のクラスを用意しました。TestData クラスが複製対象となるオブジェクトです。

public sealed class TestData
{
    public IList Shapes { get; } =
        new List();
}
public abstract class Shape
{
    public string? Name { get; set; }
    public virtual double Area { get; }
}
public sealed class Circle : Shape
{
    public double Radius { get; set; }
    public override double Area =>
        Radius * Radius * Math.PI;
}
public sealed class Rectangle : Shape
{
    public double Width { get; set; }
    public double Height { get; set; }
    public override double Area =>
        Width * Height;
}
var data = new TestData();
data.Shapes.Add(new Circle { Name = "Circle1", Radius = 10 });
data.Shapes.Add(new Rectangle { Name = "Rectangle1", Width = 5, Height = 4 });
Console.WriteLine("[Source]");
foreach (var shape in data.Shapes)
{
    Console.WriteLine($"  {shape.Name} => Area: {shape.Area}");
}
var cloned = ObjectCloner.Clone(data);
Console.WriteLine("[Cloned]");
foreach (var shape in cloned.Shapes)
{
    Console.WriteLine($"  {shape.Name} => Area: {shape.Area}");
}
[Source]
   Circle1 => Area: 314.1592653589793
   Rectangle1 => Area: 20
[Cloned]
   Circle1 => Area: 314.1592653589793
   Rectangle1 => Area: 20

まとめ

このように、DeepCopy.Expression ライブラリは、インターフェースや基底クラスのディープコピーにも対応しており、オブジェクトの型情報を保持しながら、高速かつ安全にディープコピーを行うことができます。

DeepCopy.Expression – メンバ内の参照関係を保持したまま複製する

  • 2023/06/02 内容を修正

今回は、メンバ内の参照を保ったままディープコピーする方法について紹介します。
ディープコピーとは、オブジェクトの内容を別のオブジェクトに完全にコピーすることです。しかし、単純にディープコピーをすると、メンバ内の参照関係が失われてしまうことがあります。
例えば、次のようなクラスがあるとします。

public sealed class TestData
{
    public void Initialize()
    {
        A = new Item("A");
        B = new Item("B", A);
    }
    public Item A { get; private set; }
    public Item B { get; private set; }
}
public sealed class Item
{
    public Item(string name, Item child = null)
    {
        Name = name;
        Child = child;
    }
    public string Name { get; set; }
    public Item Child { get; }
}

このクラスのインスタンスを単純にディープコピーすると、A プロパティと B プロパティの Child プロパティが同じオブジェクトを参照しているはずですが、コピー後は別のオブジェクトを参照してしまいます。これは、ディープコピーの際に各メンバが個別にコピーされるためです。

DeepCopy.Expression によるコピー

先程のクラスのインスタンスを DeepCopy.Expression で複製してみます。

デフォルトでは参照関係は保持されない

以下の例のように、DeepCopy.Expression の デフォルトでは内部の参照関係は保たれません。

class Program
{
    static void Main(string[] args)
    {
        var data = new TestData();
        Console.WriteLine($"Source => Ref:{data.B.Child == data.A}");
        var cloned= ObjectCloner.Clone(data);
        Console.WriteLine($"Cloned => Ref:{cloned.B.Child == cloned.A}\r\n");
 
        data.A.Name = "Original-A";
        Console.WriteLine("Set \"Orignal-A\" to the name of A in the source instance.");
        Console.WriteLine($"Source => A:\"{data.A.Name}\" , B.Child:\"{data.B.Child.Name}\"");
        Console.WriteLine($"Cloned => A:\"{cloned.A.Name}\" , B.Child:\"{cloned.B.Child.Name}\"\r\n");

        cloned.A.Name = "Cloned-A";
        Console.WriteLine("Set \"Cloned-A\" to the name of A in the cloned instance.");
        Console.WriteLine($"Source => A:\"{data.A.Name}\" , B.Child:\"{data.B.Child.Name}\"");
        Console.WriteLine($"Cloned => A:\"{cloned.A.Name}\" , B.Child:\"{cloned.B.Child.Name}\"");
    }
}
Source => Ref:True
Cloned => Ref:False

Set "Orignal-A" to the name of A in the source instance.
Source => A:"Original-A" , B.Child:"Original-A"
Cloned => A:"A" , B.Child:"A"

Set "Cloned-A" to the name of A in the cloned instance.
Source => A:"Original-A" , B.Child:"Original-A"
Cloned => A:"Cloned-A" , B.Child:"A"

複製されたインスタンスでは参照関係がなくなっている為、コピーされたオブジェクトの A プロパティの名前を変更しても、B プロパティの Child の名前は元のままです。

参照を保持したままコピーするには

メンバ内の参照を保ったままディープコピーするには、ObjectCloner.Clone メソッドの第二引数に true を指定するか、 CopyTo メソッドの第三引数に true を指定すれば出来るようになります。この引数は、内部の参照関係を保持するかどうかを示す値で、デフォルトは false になっています。

例えば次のように実行します。

class Program
{
    static void Main(string[] args)
    {
        var data = new TestData();
        Console.WriteLine($"Source => Ref:{data.B.Child == data.A}");
        var cloned= ObjectCloner.Clone(data, true);
        Console.WriteLine($"Cloned => Ref:{cloned.B.Child == cloned.A}\r\n");
 
        data.A.Name = "Original-A";
        Console.WriteLine("Set \"Orignal-A\" to the name of A in the source instance.");
        Console.WriteLine($"Source => A:\"{data.A.Name}\" , B.Child:\"{data.B.Child.Name}\"");
        Console.WriteLine($"Cloned => A:\"{cloned.A.Name}\" , B.Child:\"{cloned.B.Child.Name}\"\r\n");

        cloned.A.Name = "Cloned-A";
        Console.WriteLine("Set \"Cloned-A\" to the name of A in the cloned instance.");
        Console.WriteLine($"Source => A:\"{data.A.Name}\" , B.Child:\"{data.B.Child.Name}\"");
        Console.WriteLine($"Cloned => A:\"{cloned.A.Name}\" , B.Child:\"{cloned.B.Child.Name}\"");
    }
}
Source => Ref:True
Cloned => Ref:True

Set "Orignal-A" to the name of A in the source instance.
Source => A:"Original-A" , B.Child:"Original-A"
Cloned => A:"A" , B.Child:"A"

Set "Cloned-A" to the name of A in the cloned instance.
Source => A:"Original-A" , B.Child:"Original-A"
Cloned => A:"Cloned-A" , B.Child:"Cloned-A"

これで、ディープコピー後のオブジェクトもコピー前と同じメンバ内の参照関係を持つようになります。
例えば、data.A.Name を変更すると、 data.B.Child.Name も変更されますが、cloned.A.Name や cloned.B.Child.Name は変更されません。逆に、cloned.A.Name を変更すると、cloned.B.Child.Name も変更されますが、data.A.Name や data.B.Child.Name は変更されません。

まとめ

DeepCopy.Expression は、シリアライズを使う方法よりも高速で柔軟なディープコピーができるライブラリです。メンバ内の参照関係を保持したままディープコピーしたい場合はぜひ試してみてください。

DeepCopy.Expression – 読み取り専用のクラスメンバを持つオブジェクトをコピーする

  • 2023/5/31 内容を修正

何回かに分けて DeepCopy.Expression の紹介を書いていきます。

今回は、DeepCopy.Expression が読み取り専用のメンバを持つクラスのディープコピーに対応していることついて紹介します。

C#では、読み取り専用のプロパティやフィールドを持つクラスをシリアライズ/デシリアライズを利用してディープコピーする場合は、対象のクラス側がそれに対応する実装になっている必要があります。

このことを確認するために、以下のような _name という読み取り専用フィールドと Child と UniqueId という読み取り専用プロパティを持つクラスを用意しました。
_name と Child はコンストラクタの引数によって値が設定されますが、UniqueId はコンストラクタ内部で自動割り当てされるようにしています。

public sealed class Item
{
     private readonly string _name;

     public Item(string name, Item? child = null)
     {
         _name = name;
         Child = child;
         UniqueId = Guid.NewGuid().ToString();
     }

     public string Name => _name;
     public Item? Child { get; }
     public string UniqueId { get; }
}

シリアライザでディープコピー

このクラスを JsonSerializer を用いてディープコピーした場合は次のような結果となります。

public sealed class Item
var item = new Item("foo", new Item("bar"));

var json = JsonSerializer.Serialize(item);
var cloned = JsonSerializer.Deserialize(json);

Console.WriteLine($"[original] Name = {item.Name}, Child = {item.Child?.Name}, UniqueId = {item.UniqueId}");
Console.WriteLine($"[cloned]   Name = {cloned?.Name}, Child = {cloned?.Child?.Name}, UniqueId = {cloned?.UniqueId}");
[original] Name = foo, Child = bar, UniqueId = 5e167a0c-2f56-457e-94f7-553492500c76
[cloned]   Name = foo, Child = bar, UniqueId = 48fea065-6c9b-4c54-8150-9d621661f496

コンストラクタの引数で初期化している _name と Child はコピーされていますが、UniqueId は別の値になってしまいました。コンストラクタで初期化されないメンバも含めてディープコピーするには、対象のメンバに init アクセサを追加することで可能になります。

public sealed class Item
{
    private readonly string _name;

    public Item(string name, Item? child = null)
    {
        _name = name;
        Child = child;
        UniqueId = Guid.NewGuid().ToString();
    }

    public string Name => _name;
    public Item? Child { get; }
    public string UniqueId { get; init; }
}

これで再度シリアライザでコピーすると期待する結果が得られます。

[original] Name = foo, Child = bar, UniqueId = ea0b669a-c77e-4d0b-a899-5d1949b3201a
[cloned]   Name = foo, Child = bar, UniqueId = ea0b669a-c77e-4d0b-a899-5d1949b3201a

では、次に DeepCopy.Expression で元のクラスのインスタンスをコピーしてみます。

DeepCopy.Expression でコピー

DeepCopy.Expression は、読み取り専用のメンバも含めてディープコピーすることが出来ます。

var item = new Item("foo", new Item("bar"));
var cloned = ObjectCloner.Clone(item);

Console.WriteLine($"[original] Name = {item.Name}, Child = {item.Child?.Name}, UniqueId = {item.UniqueId}");
Console.WriteLine($"[cloned]   Name = {cloned?.Name}, Child = {cloned?.Child?.Name}, UniqueId = {cloned?.UniqueId}");
[original] Name = foo, Child = bar, UniqueId = 2f8a9900-0485-4131-9a9d-b8ee2c388dfd
[cloned]   Name = foo, Child = bar, UniqueId = 2f8a9900-0485-4131-9a9d-b8ee2c388dfd

まとめ

DeepCopy.Expression を使えば、読み取り専用メンバを持つクラスのインスタンスも、元のクラスを変更することなく、そのままディープコピーする事が可能です。

先頭に戻る