⛳
【C#】Dictionaryの使い方と落とし穴
使い方と落とし穴シリーズ一覧
関連記事
一言
-
Dictionary
を通してマッピング構造に慣れてこ~ - Unity でもよく使います
Dictionary
を使用するケース
-
キーと値のマッピングがしたい
- ハッシュ検索により最善 O(1), 最悪 O(n)で値を取得
-
高速に存在チェックをしたい
-
ContainsKey
,TryGetValue
が配列走査より速い
-
-
重複を許さない集合を扱いつつ付加情報も持たせたい
-
HashSet<T>
+List<T>
での管理よりシンプル
-
Dictionary
の使い方
基本 (生成, 追加, 取得, 削除)
//生成 (容量指定で再ハッシュを抑えられる)
var id2Enemy = new Dictionary<int, Enemy>(128);
//追加
id2Enemy[100] = new Enemy("Slime"); //重複キーなら上書き
id2Enemy.Add(200, new Enemy("Ogre")); //重複キーなら例外
id2Enemy.TryAdd(300, new Enemy("Dragon")); //重複キーでも安全
//取得
Enemy ogre = id2Enemy[200]; //キーがなければ KeyNotFoundException
if (id2Enemy.TryGetValue(300, out var dragon)) //キーがあるならtrueで、out変数に値が代入
{
Use(dragon);
}
//削除
id2Enemy.Remove(200); //戻り値の bool で成否判定も可
id2Enemy.Clear(); //要素のみ消える (容量はそのまま)
存在チェック
if (id2Enemy.ContainsKey(id) == false)
{
Debug.LogWarning($"未知のID:{id}");
return;
}
列挙
//キー,値の集合を取り出す
foreach (int id in id2Enemy.Keys) { ... }
foreach (Enemy e in id2Enemy.Values) { ... }
//タプルで取り出すことも可能 (C#7.0以降)
foreach (var (id, enemy) in id2Enemy)
{
Debug.Log($"{id}:{enemy.Name}");
}
容量管理
- 初期化時、 おおよその要素数を指定して再ハッシュ(再確保)を抑えられる
-
EnsureCapacity()
で後から容量見積もりが可能 - 不要なメモリは
TrimExcess()
で解放できる (容量に対する要素数が90%未満時)
id2Enemy.EnsureCapacity(1000);
id2Enemy.TrimExcess();
オイラはこんな落とし穴に出会った
IEquatable<T>
未実装
参照型キーで ❌ 参照比較になり、論理的に同じでもヒットしない
class Pos { public int X, Y; }
var dict = new Dictionary<Pos, string>();
dict[new Pos{X=0,Y=0}] = "origin";
Console.WriteLine(dict.ContainsKey(new Pos{X=0,Y=0})); //false
✅ 回避策
-
IEquatable<T>
を実装 orrecord class
で自動実装
可変オブジェクトをキーに使う
❌ キーのハッシュコードが途中で変わると検索不能になる
class MutableKey
{
public int Id; //可変
}
var key = new MutableKey(){ Id = 1 };
var dict = new Dictionary<MutableKey, string> {{ key, "foo" }};
key.Id = 2;
bool exists = dict.ContainsKey(key); //false
✅ 回避策
- キーは イミュータブル にする (
record
,struct
,readonly
) - 変えたいなら一度
Remove
→Add
で再登録
追加時の重複キー例外
❌ Add()
でキーが重複すると ArgumentException
発生
dict.Add(1, "first");
dict.Add(1, "second"); //ArgumentException
✅ 回避策
- 上書き or
TryAdd()
dict.TryAdd(1, "second"); //bool で結果確認
dict[1] = "second";
foreach 中に要素を変更
- foreach は列挙子を用いて
Dictionary
を読み取る
❌ foreach 中に要素が変更されると、列挙子の整合性が崩れる
❌ そのため、実行時に InvalidOperationException が発生する
foreach (var kv in dict)
{
if (kv.Key.StartsWith("tmp_"))
{
dict.Remove(kv.Key); //InvalidOperationException
}
}
✅ 回避策
-
ToList()
でコピー後に削除 orfor
ループ +Keys.ToArray()
Clear()
でメモリ解放と勘違い
❌ Clear()
は Count だけリセットし容量は残る
✅ 回避策
- 必要に応じ
TrimExcess()
を呼ぶ or 再生成
SortedDictionary
SortedDictionary<TKey, TValue>
とは
-
キー順にソートされた状態を保つ Dictionary
-
ハッシュの代わりに二分探索木を導入している
- そのため、値取得は最善で O(log n)
- ハッシュベースでないため
GetHashCode
は無関係(代わりに IComparer<TKey> が関与)
-
使用例
var sorted = new SortedDictionary<string, int>();
sorted["slime"] = 2;
sorted["ogre"] = 5;
sorted["dragon"] = 1;
foreach (var kv in sorted)
{
//dragon → ogre → slime の順で出力される
Console.WriteLine($"{kv.Key}: {kv.Value}");
}
- この例で降順にする場合、以下のクラスを
SortedDictionary
のコンストラクタに追加
class DescendingStringComparer : IComparer<string>
{
public int Compare(string? x, string? y)
=> string.Compare(y, x, StringComparison.Ordinal);
}
Discussion