げぇむぷろぐらみんぐ

日々の生活で得た知識、経験を書きます

C#のIEquatableについて学び直したまとめ

はじめに

開発中にあるクラスにIEquatableが実装されていないと指摘を受けたのですが、それについて理解が曖昧なまま実装して修正してしまったので改めて学び直したメモを残します。

IEquatableインターフェイスとは

2つのオブジェクトが等しいかどうかを調べるEqualsメソッドを実装することを保証するためのインターフェイスです。

ここでいう2つのオブジェクトが等しいという場合の等しいがどんな状態を表すかが重要で、IEquatableを実装しなくてもクラス同士の比較自体はできます。 では、実装する場合としない場合で何が違うのかを比べてみます。

IEquatableを実装しない場合とする場合の比較

実装しない場合、参照型であるクラスと値型になる構造体で挙動が変わります。 今回は参照型であるクラスについてのみ説明していきます。

以下のようなクラスが存在するとします。

namespace Sample.Equals
{
    public class EqualClass 
    {
        private int _rank;
        private string _name;

        public EqualClass(int rank, string name)
        {
            _rank = rank;
            _name = name;
        }

        public string GetRank()
        {
            return $"{_name} : Rank {_rank}";
        }
    }
}

このとき、以下のような比較をするとコメントで示されたように、falseになります。

EqualClass sample1 = new EqualClass(0, "hoge");
EqualClass sample2 = new EqualClass(0, "hoge");
            
Debug.Log(sample1.Equals(sample2)); // False

ここで、IEquatableを以下のように実装します。

public bool Equals(EqualClass equalClass)
 {
     if (equalClass == null)
     {
         return false;
     }
 
     return (this._rank == equalClass._rank && this._name == equalClass._name);
 }

そうすると、さっきの比較を行うと結果はtrueを返すようになります。

このような挙動になる理由は、Equalsを実装する前にはオブジェクトの参照先を比較しています。 そのため、先程の例ではsample1とsample2で別のインスタンスが作られているため、中の値が等しくてもfalseを返すようになっているのです。 しかし、Equalsを実装した場合はその中身でそれぞれのメンバの値を比較して結果を返しているため、値が等しければtrueを返します。

このように、直接メンバの値によって比較したい場合は、IEquatableインターフェイスを実装する必要があります。

IEquatableを実装すべき場合

先程説明したように、参照型のオブジェクトで値の比較をしたい場合はEqualsの実装が必要です。 では、このように値の比較をしたくなるのはどんな場合にあるのでしょうか?

大きなものの一つに、ListやArray、Dictionaryといったジェネリックなコレクションクラスで利用したい場合が挙げられます。 例えば、先程のEqualClassを下記のようにListでまとめたかったとします。

List<EqualClass> sampleList = new List<EqualClass>()
{
    new EqualClass(0, "hoge"),
    new EqualClass(1, "fuga"),
};

ここで、rankが0で名前がhogeだった人がリストの何番目に入っているか調べたくなったときに、以下のように記述した際に、IEquatableが実装されていないと見つからなかったことになり-1を返します。

EqualClass hoge = new EqualClass(0, "hoge");
            
Debug.Log(sampleList.IndexOf(hoge));    // -1

このように、意図せず比較を使っている場合もあるため常にIEquatableは実装したほうが良いと思います。

余談ですが、Dictionaryのキーに使いたい場合は、IEquatableインターフェイスの他にGetHashCode()もオーバーライドする必要があります。

IEquatableとIEquatable(Object)の違い

このように比較をする可能性のあるクラスには必ず実装しておいたほうが良さそうなIEquatableですが、二種類あります。 この2つの違いは何なのでしょうか?

2つのEqualsは以下のようなメソッドになっています。

bool IEquatable<T>(T obj)
bool IEquatable(object obj)

前者は先程まで説明していたジェネリックなIEquatableで後者は引数にobject型を取るEqualsです。 これらの違いとしては、objectの方は比較の際にボクシングが発生するためパフォーマンス的にあまり良くないですが、ジェネリックな方はボクシングが発生しない分パフォーマンスが良くなります。

一見objectの方はパフォーマンスも悪く、使うタイミングがないように思えますが、クラスによってはobjectの方のみを呼び出すようになっているものもあるため、常にジェネリックの方を実装していたとしてもobjectの方も実装したほうが良いそうです。

まとめ

理解が曖昧だったEqualについて整理しました。 こうやってまとめて振り返ってみると、今回指摘されたのはListで利用する可能性のあるクラスだったからでした。 こんな感じで日々の開発の中でふと疑問に思ったことはしっかりと時間の取れるときに調べてまとめることでより理解を深めていきたいですね。 間違っているところがあれば、是非コメントください。

参考

IEquatable(T) インターフェイス (System)

自作クラスのEqualsメソッドをオーバーライドして、等価の定義を変更する - .NET Tips (VB.NET,C#...)