C#9.0から使えるrecordで独自のEqualsやDeepCloneを使いたい

# レコード型とは

レコード型というものを今日知りました。
MSDN
https://docs.microsoft.com/ja-jp/dotnet/csharp/whats-new/tutorials/records

# なぜ記事を書いたか

プロパティをImmutableにするためにinit専用セッターを使いたいけど、DeepCloneするときにinit専用だと参照型の上書きができずに困ってしまいます。
そんな時にレコード型を使えばinit専用セッターを使いつつDeepCloneができそうだと思い書きました。

~~~ C#
    public sealed class Point : ICloneable
    {
        public double X { get; set; }
        public double Y { get; set; }

        public object Clone()
        {
            return (Point) MemberwiseClone();
        }
    }

    public sealed class Enemy : ICloneable
    {
        public int Life { get; init; }
        public Point P { get; init; } // init

        public object Clone()
        {
            var clone = (Enemy) MemberwiseClone();
            clone.P = (Point) P.Clone(); // !!!! initだと設定できん !!!!
            return clone;
        }
    }

    class Program
    {
        static void Main(string[] args)
        {
            var enemy = new Enemy()
            {
                Life = 100,
                P = new Point() {X = 0.000234, Y = 0.00000123,}
            };

            var clone = (Enemy)enemy.Clone();
        }
    }
~~~

# やったこと

## とりあえずclassをrecordに変更してみよ

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/675344/cc1ddc94-5f92-6c9e-6c92-bc6ac334e970.png)

なんやと。「Cloneという名前のメンバーはレコードでは許可されていません。」
recordはCloneやEqualsをすでに定義しているらしい。
おなじみ++C++を見てね。
[++C++（レコード型）](https://ufcpp.net/study/csharp/datatype/record/)

## しょうがないかから適当にDeepClone用のIF作って実装しよ

こうや。

~~~ C#
    public interface IDeepClone
    {
        object DeepClone();
    }

    public sealed record Point : IDeepClone
    {
        public double X { get; set; }
        public double Y { get; set; }
        public object DeepClone() // Implementation of IDeepClone
        {
            return this with { };
        }
    }

    public sealed record Enemy : IDeepClone
    {
        public int Life { get; init; }
        public Point P { get; init; }
        public object DeepClone() // Implementation of IDeepClone
        {
            return this with {P = this.P with { }}; // 中で保持しているrecordをwithで複製してDeepCloneしてる
        }
    }

    class Program
    {
        static void Main(string[] args)
        {
            var enemy = new Enemy()
            {
                Life = 100,
                P = new Point() {X = 0.000234, Y = 0.00000123,}
            };

            var clone = enemy with { }; // コッチはShallow
            var deepClone = (Enemy)enemy.DeepClone(); // コッチはDeep

            enemy.P.X = 99999;

            Console.WriteLine($"Enemy     :Life={enemy.Life}, P.X={enemy.P.X}, P.Y={enemy.P.Y}");
            Console.WriteLine($"Clone     :Life={clone.Life}, P.X={clone.P.X}, P.Y={clone.P.Y}");
            Console.WriteLine($"DeepClone :Life={deepClone.Life}, P.X={deepClone.P.X}, P.Y={deepClone.P.Y}");
        }
    }
~~~

念のため実行結果。
enemy.P.Xに99999設定してますが、Cloneには反映されてDeepCloneは反映されてない。

~~~
Enemy     :Life=100, P.X=99999, P.Y=1.23E-06
Clone     :Life=100, P.X=99999, P.Y=1.23E-06
DeepClone :Life=100, P.X=0.000234, P.Y=1.23E-06
~~~

## 浮動小数点とか完全一致じゃなくて、ちょっとの変更は同じにしちゃいたい（Equalsカスタマイズしたい）

bool Equals(object obj)をoverrideできないのでbool Equals(T obj)を作った。
GetHashCodeも作った。
↓こうや。

~~~ C#
    public interface IDeepClone
    {
        object DeepClone();
    }

    public sealed record Point : IDeepClone
    {
        public double X { get; set; }
        public double Y { get; set; }

        public bool Equals(Point other)
        {
            if (Math.Abs(this.X - other.X) > 0.0001) return false;
            if (Math.Abs(this.Y - other.Y) > 0.0001) return false;
            return true;
        }

        public override int GetHashCode()
        {
            return new {X, Y}.GetHashCode();
        }

        public object DeepClone() // Implementation of IDeepClone
        {
            return this with { };
        }
    }

    public sealed record Enemy : IDeepClone
    {
        public int Life { get; init; }
        public Point P { get; init; }

        public object DeepClone() // Implementation of IDeepClone
        {
            return this with {P = this.P with { }}; // 中で保持しているrecordのインスタンスをwithで複製してDeepCloneしてる
        }
    }

    class Program
    {
        static void Main(string[] args)
        {
            {
                Console.WriteLine("検証1:withで複製した場合とDeepClone(値の変更無し)");
                var enemy = new Enemy()
                {
                    Life = 100,
                    P = new Point() {X = 0.000234, Y = 0.00000123,}
                };

                var clone = enemy with { };
                var deepClone = (Enemy) enemy.DeepClone();

                Console.WriteLine($"Enemy     :Life={enemy.Life}, P.X={enemy.P.X}, P.Y={enemy.P.Y}");
                Console.WriteLine($"Clone     :Life={clone.Life}, P.X={clone.P.X}, P.Y={clone.P.Y}");
                Console.WriteLine($"DeepClone :Life={deepClone.Life}, P.X={deepClone.P.X}, P.Y={deepClone.P.Y}");

                Console.WriteLine($"元のインスタンスとwithで複製したインスタンスを比較        : {enemy.Equals(clone)}");
                Console.WriteLine($"元のインスタンスとDeepCloneで複製したインスタンスを比較   : {enemy.Equals(deepClone)}");
                Console.WriteLine();
            }

            {
                Console.WriteLine("検証2:withで複製した場合とDeepClone(値を変更)");
                var enemy = new Enemy()
                {
                    Life = 100,
                    P = new Point() {X = 0.000234, Y = 0.00000123,}
                };

                var clone = enemy with { };
                var deepClone = (Enemy) enemy.DeepClone();

                enemy.P.X = 0.000999;

                Console.WriteLine($"Enemy     :Life={enemy.Life}, P.X={enemy.P.X}, P.Y={enemy.P.Y}");
                Console.WriteLine($"Clone     :Life={clone.Life}, P.X={clone.P.X}, P.Y={clone.P.Y}");
                Console.WriteLine($"DeepClone :Life={deepClone.Life}, P.X={deepClone.P.X}, P.Y={deepClone.P.Y}");

                Console.WriteLine($"元のインスタンスとwithで複製したインスタンスを比較        : {enemy.Equals(clone)}");
                Console.WriteLine($"元のインスタンスとDeepCloneで複製したインスタンスを比較   : {enemy.Equals(deepClone)}");
                Console.WriteLine();
            }

            {
                Console.WriteLine("検証3:withで複製した場合とDeepClone(値をちょっとだけ変更)");
                var enemy = new Enemy()
                {
                    Life = 100,
                    P = new Point() {X = 0.000234, Y = 0.00000123,}
                };

                var clone = enemy with { };
                var deepClone = (Enemy) enemy.DeepClone();

                enemy.P.X += 0.00004321;

                Console.WriteLine($"Enemy     :Life={enemy.Life}, P.X={enemy.P.X}, P.Y={enemy.P.Y}");
                Console.WriteLine($"Clone     :Life={clone.Life}, P.X={clone.P.X}, P.Y={clone.P.Y}");
                Console.WriteLine($"DeepClone :Life={deepClone.Life}, P.X={deepClone.P.X}, P.Y={deepClone.P.Y}");

                Console.WriteLine($"元のインスタンスとwithで複製したインスタンスを比較        : {enemy.Equals(clone)}");
                Console.WriteLine($"元のインスタンスとDeepCloneで複製したインスタンスを比較   : {enemy.Equals(deepClone)}");
                Console.WriteLine();
            }
        }
~~~

結果。Clone/DeepCloneの違いと、独自Equalsが効いていることが確認できた。

~~~
検証1:withで複製した場合とDeepClone(値の変更無し)
Enemy     :Life=100, P.X=0.000234, P.Y=1.23E-06
Clone     :Life=100, P.X=0.000234, P.Y=1.23E-06
DeepClone :Life=100, P.X=0.000234, P.Y=1.23E-06
元のインスタンスとwithで複製したインスタンスを比較        : True
元のインスタンスとDeepCloneで複製したインスタンスを比較   : True

検証2:withで複製した場合とDeepClone(値を変更)
Enemy     :Life=100, P.X=0.000999, P.Y=1.23E-06
Clone     :Life=100, P.X=0.000999, P.Y=1.23E-06
DeepClone :Life=100, P.X=0.000234, P.Y=1.23E-06
元のインスタンスとwithで複製したインスタンスを比較        : True
元のインスタンスとDeepCloneで複製したインスタンスを比較   : False

検証3:withで複製した場合とDeepClone(値をちょっとだけ変更)
Enemy     :Life=100, P.X=0.00027721, P.Y=1.23E-06
Clone     :Life=100, P.X=0.00027721, P.Y=1.23E-06
DeepClone :Life=100, P.X=0.000234, P.Y=1.23E-06
元のインスタンスとwithで複製したインスタンスを比較        : True
元のインスタンスとDeepCloneで複製したインスタンスを比較   : True
~~~

おわり