System.Collections.Generic.Dictionary<TKey, TValue> は、C# で 連想配列(キーから値への高速ルックアップ) を扱うときの第一選択です。.NET 2.0 のジェネリック導入時に追加されて以来、20 年以上にわたって標準辞書として使われ続けています。本記事では内部実装、サポートインターフェース、性能特性、そして実務での使いどころを整理します。
Dictionary<TKey, TValue> とは
- 名前空間:
System.Collections.Generic - 内部構造:分離チェイニング(バケット配列+エントリ配列)。同じバケットに落ちた要素はリンクで繋がる
- キー比較:
IEqualityComparer<TKey>(既定はEqualityComparer<TKey>.Default) - 列挙順は 挿入順を保つ実装 になっているが、仕様としては保証されない(将来の最適化で変わり得る)
- スレッドセーフでない
var prices = new Dictionary<string, int>
{
["apple"] = 150,
["banana"] = 100,
};
prices["cherry"] = 300;
if (prices.TryGetValue("apple", out int p))
Console.WriteLine(p);
サポートするインターフェース
| インターフェース | 役割 |
|---|---|
IDictionary<TKey, TValue> |
ジェネリックな連想コレクション |
IReadOnlyDictionary<TKey, TValue> |
読み取り専用ビュー |
ICollection<KeyValuePair<TKey, TValue>> |
要素単位での Count / Add / Remove 等 |
IReadOnlyCollection<KeyValuePair<TKey, TValue>> |
読み取り専用の Count |
IEnumerable<KeyValuePair<TKey, TValue>> |
foreach で KeyValuePair を列挙 |
IDictionary / ICollection / IEnumerable |
非ジェネリック版(互換のため) |
ISerializable / IDeserializationCallback |
バイナリシリアライズ対応 |
IDictionary(非ジェネリック)の詳細は Hashtable の記事、IEnumerable / IEnumerator は IEnumerable と IEnumerator の記事 を参照してください。
IDictionary<TKey, TValue> は非ジェネリック版とほぼ同じ構成ですが、戻り値が KeyValuePair<TKey, TValue> 構造体で、ボックス化なし・型安全です。
public interface IDictionary<TKey, TValue> : ICollection<KeyValuePair<TKey, TValue>>
{
TValue this[TKey key] { get; set; }
ICollection<TKey> Keys { get; }
ICollection<TValue> Values { get; }
void Add(TKey key, TValue value);
bool Remove(TKey key);
bool ContainsKey(TKey key);
bool TryGetValue(TKey key, out TValue value);
}
内部実装と性能特性
Dictionary<TKey, TValue> は 分離チェイニング(separate chaining)方式のハッシュテーブルです。
key.GetHashCode()を呼んでハッシュ値を計算- ハッシュ値からバケット(インデックス)を決定
- 同じバケットにあるエントリリストを線形に辿り、
comparer.Equalsでキーを比較
衝突しても 既存エントリ配列内のインデックスでリンク していくため、ノード割り当てが発生せず、キャッシュ効率が良いのが特徴です。
| 操作 | 平均 | 最悪 |
|---|---|---|
[key] 取得 |
O(1) | O(n) |
Add / 代入 |
O(1) 償却 | O(n) |
Remove |
O(1) | O(n) |
ContainsKey |
O(1) | O(n) |
ContainsValue |
O(n) | O(n) |
| 列挙 | O(n) | O(n) |
最悪 O(n) は同じハッシュに大量に衝突したとき。実務では キーの GetHashCode がまともに分散していれば 平均 O(1) を信じて問題ありません。
容量と再ハッシュ
要素数が容量を超えると、内部配列を約 2 倍(厳密には次の素数)に拡張して全要素を再配置します。事前に要素数の見当が付くなら容量を指定 すると、再ハッシュ回数を減らせます。
var dict = new Dictionary<string, int>(capacity: 100_000);
.NET 5 以降は EnsureCapacity(n) で動的に容量確保もできます。
dict.EnsureCapacity(1_000_000);
キーの等値性 — IEqualityComparer<TKey>
キーの比較は IEqualityComparer<TKey> がすべて決めます。文字列キーで大文字小文字を無視したいなら StringComparer.OrdinalIgnoreCase を渡すのが定番です。
var dict = new Dictionary<string, int>(StringComparer.OrdinalIgnoreCase);
dict["Apple"] = 1;
Console.WriteLine(dict["APPLE"]); // 1
カスタム型をキーにするなら、Equals と GetHashCode を一貫して実装 してください。record 型なら自動生成されます。
安全な API — TryGetValue / TryAdd / GetValueOrDefault
存在しないキーに [key] でアクセスすると KeyNotFoundException。例外コストを避けるための安全 API が用意されています。
// 1) 取得(存在チェック込み)
if (dict.TryGetValue("apple", out int price))
Console.WriteLine(price);
// 2) 追加(既にあれば失敗を返すだけで例外を投げない)
bool added = dict.TryAdd("banana", 100);
// 3) 既定値付き取得(.NET Standard 2.1 / .NET Core 2.0+)
int p = dict.GetValueOrDefault("kiwi", defaultValue: 0);
Add(key, value) は重複キーで ArgumentException を投げます。「重複してもいい」なら [key] = value を、「重複したら知りたい」なら Add を 使い分けます。
ref 戻りでの直接アクセス(CollectionsMarshal)
.NET 6+ では、辞書の値スロットに ref で直接アクセス する API があります。値型のカウンタを更新するときに、ボックス化なしで効率的に書けます。
using System.Runtime.InteropServices;
ref int count = ref CollectionsMarshal.GetValueRefOrAddDefault(dict, key, out bool exists);
count++;
「キーごとの集計」「キャッシュのカウンタ」のようなホットパスで覚えておくと役に立ちます。
Hashtable / ConcurrentDictionary との比較
| 観点 | Dictionary<TKey, TValue> |
Hashtable |
ConcurrentDictionary<TKey, TValue> |
|---|---|---|---|
| 型安全 | ◯ | × object |
◯ |
| 値型のボックス化 | なし | あり | なし |
| スレッドセーフ | × | △(Synchronizedラッパ) | ◯(細粒度ロック / lock-free read) |
| 推奨 | 単一スレッドの標準辞書 | レガシー互換 | 並行アクセス用 |
並行で使うなら最初から ConcurrentDictionary<TKey, TValue> を選ぶか、自前で lock を行います。読み取りすらスレッドセーフではない 点に注意(ある瞬間に他スレッドが書き換え中だと内部状態が壊れた値を読みうる)。
使いどころ
- ID から名前のルックアップ、設定の連想配列、キャッシュ、カウンタ集計など、「キー → 値」が必要なほぼすべての場面
- 大量データを高速に検索したいとき
- イベント通知の購読者リスト(キー:イベント名、値:ハンドラ)など
「順序が必要」なら SortedDictionary<TK,TV> や OrderedDictionary、「並行が必要」なら ConcurrentDictionary<TK,TV> を検討します。
注意点
- 列挙中に書き換えると
InvalidOperationException:foreachのあいだにAdd/Removeしてはいけない。書き換えるならキーリストをToList()してから回す。 - キーは可変なオブジェクトにしない:辞書に入れた後にハッシュ値が変わると見つけられなくなる。
Keys/Valuesプロパティは生のビュー:辞書を変更すると次回列挙時に反映されるが、列挙中の変更は例外。- DoS 対策のハッシュランダム化:
StringComparer.Ordinalなどを使うことで攻撃者制御の入力に対する衝突攻撃を緩和できる。.NET 4.5+ では既定の文字列ハッシュにランダム化が入っている場合がある。
まとめ
Dictionary<TKey, TValue>は C# の標準連想配列。分離チェイニングで平均 O(1)。- ジェネリックなインターフェース群(
IDictionary<,>/IReadOnlyDictionary<,>/ICollection<KVP>等)に加え、互換の非ジェネリック版も実装。 - 文字列キーには
StringComparerを、独自型には正しいEquals/GetHashCodeを。 - 安全な操作には
TryGetValue/TryAdd/GetValueOrDefaultを使う。 - 並行アクセスには
ConcurrentDictionary<TKey, TValue>、順序保持にはSortedDictionary<TK,TV>を選択。