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

DictionaryのKeyに指定するオブジェクトを工夫することで複数のキーを指定できるようにしたいと思います。ただし、検索する見かける Tuple を使用した複数の値の組み合わせを Dictionary のキーに指定する方法はが見づらい & 値の意味が不明瞭化するためなるべく避けたいところです。

従って回はキーに複数の値を格納する自作クラスを作成して利用する方法を紹介したいと思います。

// Tuple をキーにする実装
Dictionary<(int, int), string> _table = new Dictionary<(int, int), string>();

// こうすると、、、

// インデックスアクセスが面倒
_table[(0, 1)] = "sample string.";

// メソッドが面倒
if(_table.ContainsKey((0, 1))) { ...

// 引数が面倒
public string Foo((int key1, int key2) key) { ...

// ★★★というか全体的に (int, int) が頻繁に出てくるが何を表してるのか意味が分からない

タプルはあくまで一時的に値を組み合わせるものであって、システムの長い期間存在するオブジェクトには使用するべきではありません。意味のある 2つの値が組み合わさった存在に、実質名前がついてない状態なのでこれがシステムの色々な場所で出現すると後から見たときに分かりにくくて追うのが大変になってしまいます。

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には一つのキーに(あと勝ち上書きになって)最後のオブジェクトだけが残っています。

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

関連記事

以下、2重 Dictionary をラップする管理クラスの実装例の紹介です。

takap-tech.com

以上です。