C#のクラスを爆速でDeepCopyする一番ナウい方法 

## はじめに
タイトル詐欺ですいません。。。
非同期処理を書いているときに、大きなListのスナップショットが欲しいと思い、DeepCopyの方法を調べてみました。
最近のC#はどんどん新しい機能が追加されているため、~~ナウい~~最新の方法を探します。

## BinaryFormatter
みんな大好きBinaryFormatter！
ですが、最新のC#ではセキュリティ上、推奨されないようです。

https://docs.microsoft.com/ja-jp/dotnet/standard/serialization/binaryformatter-security-guide#binaryformatter-security-vulnerabilities

コードはこんな感じです。

```csharp:BinaryFormatter
public static T BinaryFomartterClone<T>(T src)
{
    using (var ms = new MemoryStream())
    {
        var binaryFormatter = new System.Runtime.Serialization
            .Formatters.Binary.BinaryFormatter();
        binaryFormatter.Serialize(ms, src);
        ms.Seek(0, SeekOrigin.Begin);
        return (T)binaryFormatter.Deserialize(ms);
    }
}
```
バイト列にシリアライズしたあと、デシリアライズしています。

## JsonSerializer
どちらかといえば新顔の`System.Text.Json.JsonSerializer`です。
こちらはオブジェクトをJsonにシリアライズしたあと、デシリアライズしています。

```csharp:JsonSerializer
public static T JsonSerializerClone<T>(T src)
{
    var json = System.Text.Json.JsonSerializer.Serialize(src);
    return System.Text.Json.JsonSerializer.Deserialize<T>(json);
}
```

Json関連の処理が入るため、ぱっと見た感じは`BinaryFormatter`のほうが早そうです。
上のコードをもうちょっと省エネにするために`string`を経由せず`byte[]`と`System.Span<T>`を使ってみます。

```csharp:JsonSerializer(Use Span<T>)
public static T JsonSerializerByteClone<T>(T src)
{
    ReadOnlySpan<byte> b = System.Text.Json.JsonSerializer.SerializeToUtf8Bytes<T>(src);
    return System.Text.Json.JsonSerializer.Deserialize<T>(b);
}
```

## ベンチマーク
以下のコードを使って、どちらが早いのかベンチマークを行います。
処理内容は、`List<string>`に1000アイテム追加したものと、`List<T> where T : class`に1000アイテム追加したものをそれぞれディープコピーしていきます。

```csharp:Benchmark
// See https://aka.ms/new-console-template for more information
using BenchmarkDotNet.Attributes;
using BenchmarkDotNet.Running;
using System.Runtime.InteropServices;

var summary = BenchmarkRunner.Run<Test>();

[MemoryDiagnoser]
public class Test
{
    [Serializable]
    public class Order
    {
        public float Qty { get; set; }
        public float Price { get; set; }
    }

    private List<string> stringList;
    private List<Order> orderList;

    public Test()
    {
        stringList = new List<string>(1000);
        orderList = new List<Order>(1000);

        for(int i = 0; i < 1000; i++)
        {
            stringList.Add("testtest");
            orderList.Add(new Order() { Qty = 0.01f, Price = 0.02f });
        }
    }

    public static T BinaryFomartterClone<T>(T src)
    {
        using (var ms = new MemoryStream())
        {
            var binaryFormatter = new System.Runtime.Serialization
                .Formatters.Binary.BinaryFormatter();
            binaryFormatter.Serialize(ms, src);
            ms.Seek(0, SeekOrigin.Begin);
            return (T)binaryFormatter.Deserialize(ms);
        }
    }

    public static T JsonSerializerClone<T>(T src)
    {
        var json = System.Text.Json.JsonSerializer.Serialize(src);
        return System.Text.Json.JsonSerializer.Deserialize<T>(json);
    }

    public static T JsonSerializerByteClone<T>(T src)
    {
        ReadOnlySpan<byte> b = System.Text.Json.JsonSerializer.SerializeToUtf8Bytes<T>(src);
        return System.Text.Json.JsonSerializer.Deserialize<T>(b);
    }

    [Benchmark]
    public void BinaryFomartterCloneStringList()
    {
        var v = BinaryFomartterClone(stringList);
        if (v[0] != stringList[0]) throw new Exception();
    }

    [Benchmark]
    public void JsonSerializerCloneStringList()
    {
        var v = JsonSerializerClone(stringList);
        if (v[0] != stringList[0]) throw new Exception();
    }

    [Benchmark]
    public void JsonSerializerByteCloneStringList()
    {
        var v = JsonSerializerByteClone(stringList);
        if (v[0] != stringList[0]) throw new Exception();
    }

    [Benchmark]
    public void BinaryFomartterCloneClassList()
    {
        var v = BinaryFomartterClone(orderList);
        if (v[0].Qty != orderList[0].Qty) throw new Exception();
    }

    [Benchmark]
    public void JsonSerializerCloneClassList()
    {
        var v = JsonSerializerClone(orderList);
        if (v[0].Qty != orderList[0].Qty) throw new Exception();
    }

    [Benchmark]
    public void JsonSerializerByteCloneClassList()
    {
        var v = JsonSerializerByteClone(orderList);
        if (v[0].Qty != orderList[0].Qty) throw new Exception();
    }
}
```

## ベンチマーク結果
```
BenchmarkDotNet=v0.13.1, OS=Windows 10.0.19044.1826 (21H2)
Intel Core i7-10700K CPU 3.80GHz, 1 CPU, 16 logical and 8 physical cores
.NET SDK=7.0.100-preview.5.22307.18
  [Host]     : .NET 7.0.0 (7.0.22.30112), X64 RyuJIT
  DefaultJob : .NET 7.0.0 (7.0.22.30112), X64 RyuJIT

|                             Method |        Mean |    Error |   StdDev |   Gen 0 |   Gen 1 | Allocated |
|----------------------------------- |------------:|---------:|---------:|--------:|--------:|----------:|
|     BinaryFomartterCloneStringList |    67.21 us | 0.358 us | 0.334 us |  3.9063 |  0.1221 |     33 KB |
|      JsonSerializerCloneStringList |    71.66 us | 0.244 us | 0.217 us |  9.3994 |  1.7090 |     77 KB |
|  JsonSerializerByteCloneStringList |    71.18 us | 0.788 us | 0.737 us |  8.0566 |  0.9766 |     66 KB |
|      BinaryFomartterCloneClassList | 1,118.30 us | 6.055 us | 5.663 us | 87.8906 | 31.2500 |    721 KB |
|       JsonSerializerCloneClassList |   630.56 us | 2.378 us | 2.224 us | 10.7422 |  1.9531 |     91 KB |
|   JsonSerializerByteCloneClassList |   626.13 us | 2.501 us | 2.340 us |  7.8125 |  0.9766 |     66 KB |
```

以外にも`JsonSerializer`がすごく優秀です！
リストが複雑の場合でも、アロケーションが変わらないのもすごいです！
ディープコピーする対象が単純なリストの場合は、`BinaryFomatter`を利用してもよいかもしれません。

## まとめ
+ どんな時でも`JsonSerializer`を使うのがナウい！
+ 単純な`List`をディープコピーするときは、`BinaryFormatter`でも良いが、オブジェクトを入れたものをディープコピーするしようとするとめっちゃ遅いから注意！