C#びDictionaryで自作オブジェクトをキー:TKeyに使用する

C#のDictionaryのTKeyにオブジェクトを指定した場合、参照アドレスが同じであれば同じオブジェクトと判断されます。これはオブジェクト同士の比較で a == b が true となる場合、同じキーと認識されることを表します。

今回は、自作のクラスの内容が同じ場合同じキーと認識される方法を紹介します。

標準的な動きの確認

まず、既存の動作の確認です。以下のクラスを用意してDictionaryのキーとして使用します。

// 準備するクラス
public class KeyClass
{
    // 名前
    public string Name { get; set; }
    // 番号
    public int ID { get; set; }
}

次に、Dictionaryを作成します。

public static void Main(string[] args)
{
    var table = new Dictionary<KeyClass, string>()
    {
        // 同じ内容のオブジェクトを複数指定できる
        { new KeyClass() { Name ="Taka", ID = 0 }, "Item1" }, 
        { new KeyClass() { Name ="Taka", ID = 0 }, "Item2" }, 
        { new KeyClass() { Name ="Taka", ID = 0 }, "Item3" }, 
    };
}

正直こうなると使えないケースが多いです。入力されたオブジェクトをテーブルの中から探す場合、中身を見てから同じか判断を外ですると検索量がO(n)になりデータ構造がDictionaryじゃなくてもよいという話になってしまいます。

自作クラスの内容で判断するように修正する

キーが内容で同じか判定するための修正方法ですが、まず前提として、C#のDictionaryのオブジェクトの同一性判定は以下の条件に従います。

  • GetHashCode() で同じ値となる
  • Equals(obj) でtrue となる

したがって、自分で上記メソッドをオーバーラーイドし、オブジェクトの中身が同値であれば同じ値を返すように修正します。

// 先ほどと同じクラス
public class KeyClass
{
    // 名前
    public string Name { get; set; }
    // 番号
    public int ID { get; set; }

    // 同じ内容あれば同じ値を返すように変更
    public override int GetHashCode()
    {
        return this.Name.GetHashCode() + this.ID.GetHashCode();
    }

    // 同じ内容であればtrueを返すように変更
    public override bool Equals(object obj)
    {
        var item = obj as KeyClass;
        if(item == null)
        {
            return false;
        }

        return this.Name == item.Name && item.ID == item.ID;
    }
}

次にテーブルへ値を設定します。この場合2つ目でエラーになります。

public static void Main(string[] args)
{
    var table = new Dictionary<KeyClass, string>()
    {
        { new KeyClass() { Name ="Taka", ID = 0 }, "Item1" }, 
        { new KeyClass() { Name ="Taka", ID = 0 }, "Item2" }, 
        // → System.ArgumentException: '同一のキーを含む項目が既に追加されています。'
    };
}

演算子をオーバーロードして動きを統一する

また、上記クラスの場合、Equals と ==、!= で判定が異なってしまいます。C#では大抵の場合同じ動きが望まれるので、演算子も同じ結果を得るため以下のように演算子をオーバーロードします。

// 演算子のオーバーロードを追加
public class KeyClass
{
    // 名前
    public string Name { get; set; }
    // 番号
    public int ID { get; set; }

    // 同じ内容あれば同じ値を返す
    public override int GetHashCode()
    {
        return this.Name.GetHashCode() + this.ID.GetHashCode();
    }

    // 同じ内容であればtrueを返す
    public override bool Equals(object obj)
    {
        return this == obj as KeyClass;
    }

    // 比較演算子も同じ内容であればtrueを返すように変更
    public static bool operator ==(KeyClass a, KeyClass b)
    {
        if (a is null || b is null)
        {
            return false;
        }

        return a.Name == b.Name && a.ID == b.ID;
    }

    // == を定義すると != の定義も必要なので反対の応答をするように追加
    public static bool operator !=(KeyClass a, KeyClass b)
    {
        return !(a == b);
    }
}

動作確認のコードは以下の通りです。Equals と ==、!= で内容を見て同じか判断されます。

public static void Main(string[] args)
{
    // Item1とItem3は内容が同じ
    var item1 = new KeyClass() { Name = "Taka", ID = 0 };
    var item2 = new KeyClass() { Name = "PPPP", ID = 1 };
    var item3 = new KeyClass() { Name = "Taka", ID = 0 };

    if (item1.Equals(item3)) // 比較メソッド
    {
        Console.WriteLine("item1.Equals(item3)");
        // [Console] > item1.Equals(item3)
    }

    if(item1 == item3) // 比較判定
    {
        Console.WriteLine("item1 == item3");
        // [Console] > item1 == item3
    }

    if(item1 != item2) // 不一致判定
    {
        Console.WriteLine("item1 != item2");
        // [Console] > item1 != item2
    }

    var table = new Dictionary<KeyClass, string>();
    table.Add(item1, "item1");
    table.Add(item2, "item2");
    // → System.ArgumentException: '同一のキーを含む項目が既に追加されています。'
    table.Add(item3, "item3");
}

上記オブジェクトもDictionaryで使用すると内容で判断されます。