C#でDictionaryのキーに複数のキーを設定する

DictionaryのKeyに指定するオブジェクトを工夫することで複数のキーを指定できるようにしたいと思います。ただし、検索するとよく出てくるTupleクラスを使用した方法はコードが見づらい(というか書く値の意味が不明瞭化する)ためメリットが薄いです。従ってここでは別の方法で実現します。

Dictionaryがキーを識別する方法

その前に、Dictionaryのキーの識別方法、すなわちどのようにキーが同値かを判断するかはMSDNによると以下の通りです。

  1. Object.GetHashCode() で値が同じかどうか判定する
  2. 同じ場合 Object.Equals() 本当に同じかどうか判定する

このように2段階の確認で同じ場合同じ場所に値を格納するようになります。

従って

  1. オブジェクトに同じ値が設定されていたら同じハッシュ値を返す
  2. Equalsは内容が同じであればtrueを返す

という条件を持つ複数のフィールドからなるオブジェクトをキーに指定すれば複数の条件をキーに持たせることができたとえ言えます。

コード例

まず確認です。キーに用いるためのクラスを用意します。

// MyKey.cs

// Dictionaryのキーに使用するオブジェクト
public class MyKey
{
    public int Key1 { get; set; }
    public int Key2 { get; set; }
    public string Key3 { get; set; } // 何個かフィールドを持っている
    
    public override string ToString()
    {
        return $"{nameof(this.Key1)}={this.Key1}, " +
               $"{nameof(this.Key2)}={this.Key2}, " +
               $"{nameof(this.Key3)}={this.Key3}";
    }
}

上記クラスで次のコードを実行します。

// Program.cs
internal static void Main(string[] args)
{
    // 値を保持するテーブル
    var table = new Dictionary<MyKey, int>();

    MyKey latestKey = null;

    for (int i = 0; i < 5; i++)
    {
        var key = new MyKey()
        {
            Key1 = 0,
            Key2 = 1,
            Key3 = "a",
        };

        Console.WriteLine($"key.GetHashCode(), {key.Equals(latestKey)}");
        // > 43495525, False
        // > 55915408, False
        // > 33476626, False
        // > 32854180, False
        // > 27252167, False
        latestKey = key;
        table[key] = i;
    }
    
    foreach (var item in table)
    {
        Console.WriteLine($"[{item.Key}] => [{item.Value}]");
        // > [Key1=0, Key2=1, Key3=a] => [0]
        // > [Key1=0, Key2=1, Key3=a] => [1]
        // > [Key1=0, Key2=1, Key3=a] => [2]
        // > [Key1=0, Key2=1, Key3=a] => [3]
        // > [Key1=0, Key2=1, Key3=a] => [4]
    }
}

結果はコメントの通りですが、通常インスタンスが異なるとGetHashCodeが上記のようにひとつづつ異なっていて、Equalsもfalseを返します。Dictionaryには5つの値が格納されます。

そこで、MyKeyクラスを冒頭の条件に一致するよう、以下の通り変更します。

public class MyKey
{
    public int Key1 { get; set; }
    public int Key2 { get; set; }
    public string Key3 { get; set; }

    // ★★★同じ値で同じハッシュを返すコードを追加する
    public override int GetHashCode()
    {
        return this.Key1 ^ this.Key2 ^ this.Key3.GetHashCode();
    }

    // ★★★内容が同じであればtrueを返すコードを追加する
    public override bool Equals(object obj)
    {
        if (obj == null || !(obj is MyKey key))
        {
            return false;
        }

        return this.Key1 == key.Key1 &&
               this.Key2 == key.Key2 &&
               this.Key3 == key.Key3;
    }

    public override string ToString()
    {
        return $"{nameof(this.Key1)}={this.Key1}, " +
               $"{nameof(this.Key2)}={this.Key2}, " +
               $"{nameof(this.Key3)}={this.Key3}";
    }
}

この状態で、先ほどと同じコードを実行するとDictionaryの値の保持のされ方が変化します。

// Program.cs
internal static void Main(string[] args)
{
    // 値を保持するテーブル
    var table = new Dictionary<MyKey, int>();

    MyKey latestKey = null;

    for (int i = 0; i < 5; i++)
    {
        var key = new MyKey()
        {
            Key1 = 0,
            Key2 = 1,
            Key3 = "a",
        };

        Console.WriteLine($"{key.GetHashCode()}, {key.Equals(latestKey)}");
        table[key] = i;
        // > -231358651, False // 最初はノーカン
        // > -231358651, True
        // > -231358651, True
        // > -231358651, True
        // > -231358651, True
    }

    foreach (var item in table)
    {
        Console.WriteLine($"[{item.Key}] => [{item.Value}]");
        // [Key1=0, Key2=1, Key3=a] => [4]
        // 
        // ★★★ 全部同じキーなので最後に設定されたものが上書きされて
        // 1つのみテーブルに記録される
    }
}

同じ内容が設定される限り同じキーだとDictionaryに認識されるようになり、Dictionaryには一つのキーに(あと勝ち上書きになって)最後のオブジェクトだけが残っています。

これで、キーオブジェクトに複数の値を指定する = キーに複数の値を指定するのと実質同じことが実現できました。

以上です。