C#で自作クラス(カスタムクラス)をキーにする方法(Dictionary,HashSet)

ディクショナリの内部では、値(value)を特定するためのインデックスとしてハッシュコード(ハッシュ値)が使われています。

(1)まずGetHashCodeメソッドで、ハッシュ値が同じかを調べる
(2)ハッシュ値が同じときは、Equalsメソッドでオブジェクトの同値性を調べる
1と2が同じだったら、同じと判断する。

GetHashCodeメソッド、Equalsメソッドを自分でオーバーライドしないときは、Object型のEqualsメソッドが呼ばれます。
Object型のEqualsメソッドは参照アドレスの値を比較するみたいです。それぞれnewしたオブジェクト同士は、プロパティの値が同じでも参照アドレスが一致しないので、別物と判断されます。

◼︎ハッシュコード(ハッシュ値)
 →オブジェクトの値を元に何らかの計算を行なって求めたint型の値。

ディクショナリのキーに使うクラスの定義

例として、氏名と電話の2つをキーとして使い、2が同じ時に同じであると判断してほしいとします。

キーに使うクラスで
・GetHashCodeメソッドをオーバーライドする
・Equalsメソッドをオーバーライドする
をしておきます。

public class UserKey
{
    /// <summary>氏名</summary>
    public string Name { get; private set; }
    /// <summary>生年月日</summary>
    public string Birthday { get; private set; }
    /// <summary>電話</summary>
    public string Tel { get; private set; }

    public UserKey(string naem, string birthday, string tel)
    {
        this.Name = naem;
        this.Birthday = birthday;
        this.Tel = tel;
    }

    // 
    // overrideしておきます(´・ω・`)
    // 
    public override int GetHashCode()
    {
        // Javaでは31を乗算するのが一般的らしい
        //return Name.GetHashCode() * 31
        //        + Birthday.GetHashCode() * 31
        //        + Tel.GetHashCode() * 31;

        // C#では^演算子でXOR(排他的論理和)演算するのが普通
        return Name.GetHashCode() ^
                Birthday.GetHashCode() ^
                Tel.GetHashCode();
    }

    public override bool Equals(object obj)
    {
        // asでは、objがnullでも例外は発生せずにnullが入ってくる
        var other = obj as UserKey;
        if (other == null) return false;

        // 何が同じときに、「同じ」と判断してほしいかを記述する
        return this.Name == other.Name &&
                this.Birthday == other.Birthday &&
                this.Tel == other.Tel;
    }
}

自作クラスをディクショナリのキーに使った例

上記のUserKeyクラスをディクショナリ型のキーに使ってみた例です。

var userKeyList = new List<UserKey>()
{
    new UserKey("名前1", "20180101", "00-0000"),// 1
    new UserKey("名前2", "20180101", "00-0000"),// 2
    new UserKey("名前1", "20180101", "00-0000"),// 3(1と同じキー)
    new UserKey("名前1", "20180101", "11-1111"),// 4
};

var userDic = new Dictionary<UserKey, List<string>>();
foreach (var userKey in userKeyList)
{
    if (userDic.ContainsKey(userKey))
    {
        // GetHashCodeメソッドで一致したとき
        // Equalsメソッドでも一致するかを確認する。
        // なので両方オーバーライドしないといけない。
        continue;
    }

    userDic.Add(userKey, new List<string>());
}

HashSetでの使用例

newしているので参照アドレスが違いますが、Addメソッドで同じと判断されてます。

var hashset = new HashSet<UserKey>();

var key1 = new UserKey("名前1", "20180101", "00-0000");// 1
var key2 = new UserKey("名前1", "20180101", "00-0000");// 2(1と同じキー)
var key3 = new UserKey("名前2", "20180101", "00-0000");// 3
                                
bool isAdded1 = hashset.Add(key1);// true
bool isAdded2 = hashset.Add(key2);// false
bool isAdded3 = hashset.Add(key3);// true

演算子 ==」の場合も同様

演算子 ==」は、既定では参照の等価(オブジェクトへの参照アドレスが同じかどうか)を調べます。それぞれnewしたオブジェクト同士だと、参照アドレスが違うため元々の動作ではfalseが返ります。

Equals() と演算子 == のオーバーロードに関するガイドライン (C# プログラミング ガイド)
https://msdn.microsoft.com/ja-jp/library/ms173147(v=vs.90).aspx

var uKey1 = new UserKey("名前1", "20180101", "00-0000");// 1
var uKey2 = new UserKey("名前1", "20180101", "00-0000");// 2(1と同じキー)

// 等しいというのを調べるのも、オーバーライドしたEqualsメソッドを使えばOK
bool isEqual1 = (uKey1 == uKey2);// false
bool isEqual2 = uKey1.Equals(uKey2);// true