0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

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

Posted at

レコード型とは

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

なぜ記事を書いたか

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

    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

なんやと。「Cloneという名前のメンバーはレコードでは許可されていません。」
recordはCloneやEqualsをすでに定義しているらしい。
おなじみ++C++を見てね。
++C++(レコード型)

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

こうや。

    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も作った。
↓こうや。

    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

おわり

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?