System.Collections.Hashtable は、.NET Framework 1.0 から提供されている 非ジェネリックなキー・値ストア(連想配列) です。Dictionary<TKey, TValue>(.NET 2.0+)の登場で主役の座を譲りましたが、いまだに古い API のシグネチャや構成ファイル系のクラスで顔を出します。本記事では Hashtable の 内部構造・インターフェース・性能特性・使いどころ を整理します。
Hashtable とは
- 名前空間:
System.Collections - 内部構造:オープンアドレス法(ダブルハッシュ) によるハッシュテーブル
- キー・値はともに
object。値型はボックス化される - 既定のロードファクター(
loadFactor)は 1.0、要素数が容量を超えたら 再ハッシュ で約 2 倍に拡張 nullキーは不可(ArgumentNullException)。null値は可
using System.Collections;
var table = new Hashtable
{
["apple"] = 150,
["orange"] = 120,
};
if (table.ContainsKey("apple"))
{
int price = (int)table["apple"]!; // object → int のキャスト
Console.WriteLine(price);
}
Dictionary<TKey, TValue> がチェイニング(バケット内に連結リスト)を採用しているのに対し、Hashtable は オープンアドレス法 を採用しているのが実装上の大きな違いです。
サポートするインターフェース
| インターフェース | 役割 |
|---|---|
IDictionary |
キー・値の組を扱う連想コレクション |
ICollection |
Count / CopyTo / 同期サポート |
IEnumerable |
foreach で DictionaryEntry を列挙 |
ICloneable |
Clone() で浅いコピー |
IDictionary
IDictionary は キーと値の対応づけ を表す非ジェネリックなインターフェースです。Hashtable の中核となる API はすべてここで定義されています。
public interface IDictionary : ICollection
{
object? this[object key] { get; set; }
ICollection Keys { get; }
ICollection Values { get; }
bool Contains(object key);
void Add(object key, object? value);
void Remove(object key);
void Clear();
IDictionaryEnumerator GetEnumerator();
bool IsFixedSize { get; }
bool IsReadOnly { get; }
}
foreach で得られる要素は DictionaryEntry 構造体で、Key と Value プロパティを持ちます。
foreach (DictionaryEntry entry in table)
{
Console.WriteLine($"{entry.Key} = {entry.Value}");
}
IDictionary を実装するメリットは、構成ファイルやテンプレートエンジンなど キー・値構造を要求する API にそのまま渡せる点です(例:HashtableConverter、古い ASP.NET の ViewState まわりなど)。
ICollection / IEnumerable / ICloneable
ICollectionの詳細は ArrayList の記事 を参照。IEnumerable/IEnumeratorの詳細は IEnumerable と IEnumerator の記事 を参照。ICloneableの浅いコピーと深いコピーの違いは ICloneable インターフェースの記事 を参照。
Hashtable.Clone() は 浅いコピー。バケット配列は新規確保されますが、参照型のキー・値は同じインスタンスを共有します。
キーの等値性 — GetHashCode と Equals
Hashtable がキーを格納する際は、
key.GetHashCode()でハッシュ値を計算- ハッシュ値からバケット位置を決定
- 衝突したら別のキーと
key.Equals(other)で比較
という流れになります。キーに使う型は GetHashCode と Equals を一貫して実装 しておく必要があります。record 型や値の等価性で比較したい型では特に重要です。
カスタムの比較ロジックを使いたいときは、コンストラクタに IEqualityComparer を渡せます。
var caseInsensitive = new Hashtable(StringComparer.OrdinalIgnoreCase);
caseInsensitive["Apple"] = 1;
Console.WriteLine(caseInsensitive["APPLE"]); // 1
主な API と計算量
| 操作 | API | 平均計算量 | 最悪計算量 |
|---|---|---|---|
| 追加 | Add(key, value) / [k] = v |
O(1) 償却 | O(n)(再ハッシュ時) |
| 取得 | [key] |
O(1) | O(n)(衝突時) |
| 削除 | Remove(key) |
O(1) | O(n) |
| 検索 | ContainsKey(key) |
O(1) | O(n) |
| 値検索 | ContainsValue(value) |
O(n) | O(n) |
| 列挙 | foreach |
O(n) |
ロードファクター(既定 1.0)を下げると 衝突は減るがメモリは増える。コンストラクタで指定可能です。
var table = new Hashtable(capacity: 10_000, loadFactor: 0.7f);
ボックス化のコスト
Dictionary<TKey, TValue> と異なり、Hashtable のキー・値は両方 object。値型を扱うとキー側・値側の両方で ボックス化 が発生します。
var ht = new Hashtable();
for (int i = 0; i < 1_000_000; i++) ht[i] = i; // キーも値も各 100 万回ボックス化
100 万件規模で Dictionary<int, int> と比べると、メモリ消費・速度の両面で 数倍~10 倍程度の差 が出ることもあります。
Dictionary<TKey, TValue> との比較
| 観点 | Hashtable |
Dictionary<TKey, TValue> |
|---|---|---|
| 型安全 | × object のキャスト |
◯ コンパイル時に保証 |
| 値型のボックス化 | 発生する | 発生しない |
| 衝突解決 | オープンアドレス法 | 分離チェイニング |
| 列挙の戻り値 | DictionaryEntry |
KeyValuePair<TKey, TValue> |
| スレッドセーフ | Synchronized ラッパあり(限定的) |
通常版は非スレッドセーフ |
| 推奨度 | レガシーのみ | 新規コードの標準 |
新規コードでは 常に Dictionary<TKey, TValue>、並行アクセスが必要なら ConcurrentDictionary<TKey, TValue> を選ぶのが鉄則です。
スレッドセーフラッパ
複数スレッドからの 読み取り専用アクセスはロックなしでスレッドセーフ、書き込みを含む並行アクセスは Hashtable.Synchronized でラップします。
var sync = Hashtable.Synchronized(new Hashtable());
sync["key"] = "value"; // 各操作が暗黙でロックされる
ただし、現代の並行処理では ConcurrentDictionary<TKey, TValue> のほうが粒度の細かいロックで高性能です。
使いどころ
実務で Hashtable を選ぶ理由はほぼありません。出会うのは次のような場面です。
- レガシーコードのメンテナンス:既存 API のシグネチャが
Hashtableのまま。 - 古い ASP.NET / .NET Remoting / WCF:内部で
Hashtableを使っている API への接続。 - 構成ファイル系の互換:古い
IDictionaryベースの構成データ。 - 動的キー・値(型混在):理屈上は
Dictionary<object, object>で十分。
移行例
// Before
Hashtable ht = LoadLegacy();
foreach (DictionaryEntry e in ht)
{
string key = (string)e.Key;
int value = (int)e.Value!;
Console.WriteLine($"{key} = {value}");
}
// After(型がわかるなら直接 Dictionary に詰め替える)
var dict = ht.Cast<DictionaryEntry>()
.ToDictionary(e => (string)e.Key, e => (int)e.Value!);
foreach (var (k, v) in dict)
Console.WriteLine($"{k} = {v}");
まとめ
Hashtableは 非ジェネリックな連想配列。内部はオープンアドレス法のハッシュテーブル。IDictionary/ICollection/IEnumerable/ICloneableを実装。- 平均 O(1) アクセスだが、値型は キー・値とも にボックス化される。
- 新規コードでは
Dictionary<TKey, TValue>、並行ならConcurrentDictionary<TKey, TValue>を選ぶ。 - レガシー API との接続点で出会ったら
Cast<DictionaryEntry>().ToDictionary(...)で素早く型安全に持ち込もう。