IEquatable<T>を実装する

もくじ
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による自動実装

https://learn.microsoft.com/ja-jp/visualstudio/ide/reference/generate-equals-gethashcode-methods?view=vs-2022

record型(ufcpp)

https://ufcpp.net/study/csharp/datatype/record/

immutableとは(ufcpp)

https://ufcpp.wordpress.com/2010/05/05/immutable/

↓の本の8.6 IComparableとIEQuatableの項目