レコード型とは
レコード型というものを今日知りました。
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に変更してみよ
なんやと。「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
おわり