【C#】2つのキーを管理するDictionary

よく2つのキーを持つ Dictionary が必要になるときがありますが以下のように Dictionary を2重にすると「管理が面倒」「後から見たときに意味が分からない」など技術的負債になりがちです。

// 2つのキーで管理したいのでDictionaryを2重に宣言する
pribate Dictionary<string, Dictionary<string, MyObject> _table = new();

なので、2つのキーを管理できる Dictionary として「DualKeyDictionary」を紹介したいと思います。

作成環境

この記事は以下環境で作成しています。

  • .NET5 + C#9.0
  • VisualStudio2019
  • Windows 10

純粋な C# なので Unity 上でも使用可能です。

実装コード

DualKeyDictionaryクラス

概要

宣言は以下の通りです。

DualKeyDictionary<TKey1, TKey2, TValue>

管理したいキーを TKey1, TKey2 に指定します。

// int2つをキーにしてstringを値として管理する
DualKeyDictionary<int, int,  string> table = new();

// stringとintをキーにして自作のクラスMyObjectを値として管理する
DualKeyDictionary<string, int,  MyObject> table = new();

このクラスに実装されているメソッドは以下の通りです

メソッド名 説明
Add 要素を追加します
GetValue 要素を取得します
ContainsKey 指定した値が存在するか確認します
Remove(key1) key1に関係する要素を全て削除します
RemoveValue(key1, key2) 要素を削除します(要素が無くても例外は出ない)
Count (プロパティ)現在管理中の全ての要素数を取得します
TryCountKey1 key1に管理中のデータの個数を取得します
ClearAll 管理データを全て破棄します

以下は特殊な場合に使用します。

メソッド名 説明
SetRemoveAction 要素がオブジェクトから取り除かれる時に呼び出される処理を設定します
SetDefaultValue 値が存在しない時に返すデフォルト値を設定します(Get のとき)
実装コード

実装コードは以下の通りです。

using System;
using System.Collections.Generic;

/// <summary>
/// 2つのキーを持つデータを管理します。
/// </summary>
public class DualKeyDictionary<TKey1, TKey2, TValue>
{
    // 
    // Fields
    // - - - - - - - - - - - - - - - - - - - -

    private Dictionary<TKey1, Dictionary<TKey2, TValue>> _table = new();
    private Action<TValue> _removeAction;
    private TValue _defaultValue = default;

    // 
    // Methods
    // - - - - - - - - - - - - - - - - - - - -

    /// <summary>
    /// 要素がオブジェクトの管理テーブルから取り除かれる時に呼び出される処理を設定します。
    /// </summary>
    public void SetRemoveAction(Action<TValue> removeAction)
    {
        _removeAction = removeAction;
    }

    /// <summary>
    /// 値が存在しない時に返すデフォルト値を設定します。
    /// </summary>
    public void SetDefaultValue(TValue value)
    {
        _defaultValue = value;
    }

    /// <summary>
    /// 要素を追加します。
    /// </summary>
    public void Add(TKey1 key1, TKey2 key2, TValue value)
    {
        if (!_table.ContainsKey(key1))
        {
            _table[key1] = new Dictionary<TKey2, TValue>();
        }

        var sub = _table[key1];
        if (sub.ContainsKey(key2))
        {
            TValue item = sub[key2];
            _removeAction?.Invoke(item);
        }

        sub[key2] = value;
    }

    /// <summary>
    /// 要素を取得します。
    /// </summary>
    public TValue GetValue(TKey1 key1, TKey2 key2)
    {
        if (!_table.ContainsKey(key1))
        {
            return _defaultValue;
        }

        var sub = _table[key1];
        if (!sub.ContainsKey(key2))
        {
            return _defaultValue;
        }

        return sub[key2];
    }

    /// <summary>
    /// 指定した値が存在するか確認します。
    /// </summary>
    public bool ContainsKey(TKey1 key1, TKey2 key2)
    {
        if (!_table.ContainsKey(key1))
        {
            return false;
        }
        return _table[key1].ContainsKey(key2);
    }

    /// <summary>
    /// 要素を削除します。
    /// </summary>
    public void Remove(TKey1 key1)
    {
        if (!_table.ContainsKey(key1))
        {
            return;
        }
        try
        {
            foreach (var item in _table[key1].Values)
            {
                _removeAction?.Invoke(item);
            }
        }
        finally
        {
            _table.Remove(key1);
        }
    }

    /// <summary>
    /// 要素を削除します。
    /// </summary>
    public void Remove(TKey1 key1, TKey2 key2)
    {
        if (!_table.ContainsKey(key1))
        {
            return;
        }

        var sub = _table[key1];
        if (!sub.ContainsKey(key2))
        {
            return;
        }

        try
        {
            TValue item = sub[key2];
            _removeAction?.Invoke(item);
        }
        finally
        {
            sub.Remove(key2);
        }
    }

    /// <summary>
    /// 現在管理中の全ての要素数を取得します。
    /// </summary>
    public int Count
    {
        get
        {
            int count = 0;
            foreach (var sub in _table.Values)
            {
                count += sub.Count;
            }
            return count;
        }
    }

    /// <summary>
    /// <see cref="TKey2"/> で管理されているデータの個数を取得します。
    /// </summary>
    public bool TryCountKey1(TKey1 key1, out int count)
    {
        if (_table.ContainsKey(key1))
        {
            count = _table[key1].Count;
            return true;
        }

        count = -1;
        return false;
    }

    /// <summary>
    /// 管理データを全て破棄します。
    /// </summary>
    public void ClearAll()
    {
        try
        {
            foreach (Dictionary<TKey2, TValue> sub in _table.Values)
            {
                foreach (TValue item in sub.Values)
                {
                    _removeAction?.Invoke(item);
                }
            }
        }
        finally
        {
            foreach (var item in _table.Values)
            {
                item.Clear();
            }
            _table.Clear();
            _table.TrimExcess();
        }
    }
}

使い方

2つの int をキーに持ち、string を値をして管理する場合以下のような形式になります。

static void Main(string[] args)
{
    // 2つのintがキーでstringを値として管理する
    DualKeyDictionary<int, int, string> table = new();

    // テーブルから管理が外れたときに呼び出される処理の登録
    table.SetRemoveAction(item => Console.WriteLine($"Remove={item}"));

    // 値を追加
    table.Add(0, 0, "aaaa");
    table.Add(0, 1, "aazz");
    table.Add(0, 2, "yyyy");
    table.Add(0, 3, "xxxx");
    table.Add(1, 0, "bbbb");
    table.Add(1, 1, "bbbc");

    // 存在する値を上書き
    table.Add(0, 0, "aaab"); // > Remove=aaaa
    table.Add(0, 0, "aaac"); // > Remove=aaab

    // 値の存在確認
    bool a1 = table.ContainsKey(0, 0); // a1=true
    bool a2 = table.ContainsKey(0, 1); // a2=true

    // 値を削除
    table.Remove(0, 0); // > Remove=aaac
    table.Remove(0, 1); // > Remove=aazz

    // 値の存在確認
    bool b1 = table.ContainsKey(0, 0); // b1=false
    bool b2 = table.ContainsKey(0, 1); // b2=false

    // 値の取得
    var r1 = table.GetValue(1, 0); // r1=bbbb
    var r2 = table.GetValue(1, 1); // r2=bbbc

    // 存在しない値を要求, 例外は出ない
    var r3 = table.GetValue(999, 999); // r3=null, stringのデフォルト値

    // 存在しない値を要求したときの応答値を変更
    table.SetDefaultValue("990909090");
    var r4 = table.GetValue(999, 999); // r4=990909090

    // 管理中のデータ個数を取得
    int c1 = table.Count; // c1=4
    if (table.TryCountKey1(0, out int c2))
    {
        // c2=2
    }

    // 値の削除, key1=0に関係する値を全て削除
    table.Remove(0);
    // >Remove=yyyy
    // >Remove=xxxx

    // 全ての値をクリア
    table.ClearAll();
    // > Remove=bbbb
    // > Remove=bbbc

    // 管理中のデータ個数を取得
    int c3 = table.Count; // c3=0
}

これを使用すれば意味不明になりがちな2重 Dictionary は使用しなくて良くなります。3重 Dictionary とかもありますが、そうなるともう設計ミスの可能性が高いので少し考え直した方がいいかもしれませんね。

以上です。