IEquatable と GetHashCode() はセットで実装

概要

.NET (C#, VB.NET) で、ジェネリックコレクションに独自の型を格納するときは、 IEquatable や IComparable と GetHashCode() の実装を行います。

GetHashCode() の実装は忘れやすいため、注意が必要です。これらを実装することで、独自の型でキーが同じかどうかを確認できるようになります。

背景

MSDN で Dictionary の解説を見ると、以下のような記述があります。

Dictionary は、キーが同じであるかどうかを確認するための等値比較の実装を必要とします。comparer パラメータを受け付けるコンストラクタを使用して、IEqualityComparer ジェネリック インターフェイスの実装を指定できます。実装を指定しない場合は、既定のジェネリック等値比較演算子である EqualityComparer.Default が使用されます。型 TKey が System.IEquatable ジェネリック インターフェイスを実装している場合は、既定の等値比較演算子でその実装が使用されます。

この記述だけを見ると、作成したクラスに IEquatable さえ実装すれば Dictionary のキーにしても大丈夫な印象を受けますが、実際には GetHashCode() も実装する必要があります。

試しに GetHashCode() の実装を行わず IEquatable だけ実装して IEquatable.Equals(T other) にブレークポイントを置いても、その位置で実行が止まらないことが確認できます。

Dictionary だけでなく List などのジェネリックコレクション全般にいえることですので、 GetHashCode() をオーバーライドするのを忘れないように注意しましょう。

サンプル

IEquatable を実装するサンプルを以下に示します。このサンプルでは、名前と誕生日を持った Person クラスを作成し、 IEquatable を実装しています。同時に、 GetHashCode() をオーバーライドしています。 *1

public class Person : IEquatable<Person>
{
    private string name;
    private DateTime birthday;

    public string Name
    {
        get { return name; }
    }

    public DateTime Birthday
    {
        get { return birthday; }
    }

    public Person(string name, DateTime birthday)
    {
        this.name = name;
        this.birthday = birthday;
    }

    // 実装するのを忘れやすいので注意
    public override int GetHashCode()
    {
        return name.GetHashCode() ^ birthday.GetHashCode();
    }

    #region IEquatable<Person> メンバ

    bool IEquatable<Person>.Equals(Person other)
    {
        if (other == null)
        {
            return false;
        }

        return name == other.name && birthday == other.birthday;
    }

    #endregion
}

Dictionary のキーとして作成した Person を使用するサンプルを以下に示します。この例では、 Person を住所に関連づけています。 *2

実行すると「address: Osaka」が返され、比較が正しく行われていることがわかります。

Dictionary<Person, string> addressBook = new Dictionary<Person, string>();

addressBook.Add(new Person("foo", new DateTime(2007, 3, 4)), "Tokyo");
addressBook.Add(new Person("bar", new DateTime(2007, 4, 1)), "Osaka");
addressBook.Add(new Person("bar", new DateTime(2007, 4, 2)), "Hokkaido");

string address;

if (addressBook.TryGetValue(new Person("bar", new DateTime(2007, 4, 1)), out address))
{
    System.Diagnostics.Debug.WriteLine("address: " + address);
}
else
{
    System.Diagnostics.Debug.WriteLine("address: not found");
}

// [実行結果]
// address: Osaka

*1:役に立たないクラスですが、サンプルなので……

*2:IEquatable と GetHashCode() の説明をするためだけの、意味のないサンプルです。