もくじ
https://tera1707.com/entry/2022/02/06/144447
やりたいこと
図のようにデータを保存するためのクラスがあって、
その親データクラスの、新しいデータと古いデータがあったとして、
それらを比較したいときに、
- 親クラスの中にある子データクラス1と2の両方の、中の値(Data1,2,3・・・)が全部一致したときに「一致」と判定
- 一つでも違うものがあったら、「不一致」と判定
させたい。
で、そういうことをしたいときに、IEquatable<T>
というインターフェースを、上の図の親データクラスに実装すればよいと聞いたので、やってみたい。
やってみた
①:ともかくIEquatable<T>
実装した
とりあえず、IEquatable<T>
でやりたいことを実装してみた。
等価性の比較可能なデータクラス
namespace ConsoleApp1 { public class OyaData : IEquatable<OyaData> { private readonly KoData koData1; private readonly KoData koData2; internal OyaData(KoData data1, KoData data2) { koData1 = data1; koData2 = data2; } public bool Equals(OyaData? other) { if (other is null) return false; return (other.koData1 == this.koData1) && (other.koData2 == this.koData2); } } public record KoData(int Data1, bool Data2, bool Data3); }
KoDataのほうには、class
ではなくてrecord
を使った。
recordにすれば、今まさに使おうとしているIEquatable<T>
をすでに実装してくれていて、
(other.koData1 == this.koData1)
みたいなことが何もしなくてもできてしまうので楽。
じゃあOyaDataのクラスもrecordにすればよいじゃないかと思うのだが、OyaDataクラスの方は、
抱えているフィールドがKoData
クラスで参照型なので、単純にOyaDataのobj==OyaDataの別のobj
ということができなかった。
なので、今回の実験をした次第。
メイン関数
namespace ConsoleApp1 { internal class Program { static void Main(string[] args) { { // 一致する場合 var oya1 = new OyaData(new KoData(0, true, true), new KoData(1, false, false)); var oya2 = new OyaData(new KoData(0, true, true), new KoData(1, false, false)); Console.WriteLine($"一致:{oya1.Equals(oya2)}"); } { // 一致しない場合 var oya1 = new OyaData(new KoData(0, true, true), new KoData(1, false, false)); var oya2 = new OyaData(new KoData(1, true, true), new KoData(0, true, false)); Console.WriteLine($"一致:{oya1.Equals(oya2)}"); } } } }
これでも、それっぽく動く。
出力↓
一致:True 一致:False
②:IEquatable<T>
のお作法を守る
IEquatable<T>
を実装する際には、お作法があるらしい。(主に「.NETのクラスライブラリ設計」の本より)
- IEquatable
のEqualsを実装するときには、Object.Equalsをオーバーライドする - Box化を防いで、早いEqualsを提供するため
- Object.Equalsをオーバーライドするときには、常にGetHashCodeもオーバーライドする
- IEquatable
を実装するときには、==演算子と!=演算子のオーバーロードを検討する(必須ではないらしい)
※詳細は、「.NETのクラスライブラリ設計」の8.6と8.9.1に書いてある。
また、IEquatable<T>
を実装するクラスは、immutableであるべきらしい。今回はデータクラスなのでそれには偶然沿えてる。
で、お作法を守ってみたのがコレ。
等価性の比較可能なデータクラス
namespace ConsoleApp1 { public class OyaData : IEquatable<OyaData> { private readonly KoData koData1; private readonly KoData koData2; internal OyaData(KoData data1, KoData data2) { koData1 = data1; koData2 = data2; } public bool Equals(OyaData? other) { if (other is null) return false; return (other.koData1 == this.koData1) && (other.koData2 == this.koData2); } public override bool Equals(object? obj) { // VSのオーバーライドの自動出力で出すと、引数が(object obj)になって、null許容がもとのメソッドと // 一致しない、と警告がでる。objectのEqualsをみると、引数は(object? obj)なので、?をつければよさそう。 // VSが追い付いていない? return Equals(obj as OyaData); } public override int GetHashCode() { return HashCode.Combine(koData1, koData2); } public static bool operator ==(OyaData? left, OyaData? right) { if (left is null || right is null) return false; return left.Equals(right); } public static bool operator !=(OyaData? left, OyaData? right) { if (left is null || right is null) return false; return !left.Equals(right); } } public record KoData(int Data1, bool Data2, bool Data3); }
メイン関数
namespace ConsoleApp1 { internal class Program { static void Main(string[] args) { { // 一致する場合 Console.WriteLine("一致する場合"); var oya1 = new OyaData(new KoData(0, true, true), new KoData(1, false, false)); var oya2 = new OyaData(new KoData(0, true, true), new KoData(1, false, false)); Console.WriteLine($" 一致:{oya1.Equals(oya2)}"); } { // 一致しない場合 Console.WriteLine("一致しない場合"); var oya1 = new OyaData(new KoData(0, true, true), new KoData(1, false, false)); var oya2 = new OyaData(new KoData(1, true, true), new KoData(0, true, false)); Console.WriteLine($" 一致:{oya1.Equals(oya2)}"); } { // 一致する場合(==) Console.WriteLine("==のオーバーロード"); var oya1 = new OyaData(new KoData(0, true, true), new KoData(1, false, false)); var oya2 = new OyaData(new KoData(0, true, true), new KoData(1, false, false)); Console.WriteLine($" 一致 :{oya1== oya2}"); Console.WriteLine($" 一致しない:{oya1 != oya2}"); } { // 一致しない場合(!=) Console.WriteLine("!=のオーバーロード"); var oya1 = new OyaData(new KoData(0, true, true), new KoData(1, false, false)); var oya2 = new OyaData(new KoData(1, true, true), new KoData(0, true, false)); Console.WriteLine($" 一致 :{oya1 == oya2}"); Console.WriteLine($" 一致しない:{oya1 != oya2}"); } } } }
余談:IComparable<T>
じゃないのか?
似たようなinterfaceで、IComparable<T>
というのがあって、比較をさせたいときに実装するのはIComparable<T>
だっけ?IEquatable<T>
だっけ?となったが、IEquatable<T>
が正しかった。
IComparable<T>
は大小比較のために使われ、IEquatable<T>
は等価性の比較のために使われる様子。
特記事項
GetHashCodeの書き方
はっきり言って、GetHashCodeを何のためにオーバーライドするのか、どういう役割を果たすのか、理解できてない。
が、下記のようにすると、VisualStudioが自動で出力してくれる。
- クラス名にカーソルを合わせて、ALT+Enterを押す。
- 出てきたリストの中の「GetHashCode()を生成する」を押す
- 下図のような画面が出るので、OKを押す。
→これで、上のコードのような、GetHashCodeを生成してくれる。
(何のためにGetHashCodeがそんざいするか、は後追いで勉強、、、)
==と!=のオーバーロード
これは、まぁそういう風にした方がよいのだろうな、と思う。
oya1 == oya2
みたいな書き方ができた方が、すごく直感的なので。
以上
これで、今回やりたかったことは一通りできたと思う。
参考
IEquatableを完全に理解する
https://qiita.com/cactuaroid/items/f277a097ecf51eb247c0
GetHashCodeのVisualStudioによる自動実装
record型(ufcpp)
https://ufcpp.net/study/csharp/datatype/record/
immutableとは(ufcpp)
https://ufcpp.wordpress.com/2010/05/05/immutable/
↓の本の8.6 IComparable