`Hashtable` 與 `ArrayList` 是 .NET Framework 1.x 時代的非泛型集合標誌。雖然 .NET 2.0+ 引入泛型 (`Dictionary<TKey, TValue>`、`List<T>`) 徹底取代了它們,但在**維護舊代碼、反射/動態場景、特定序列化協議**中仍會遇到。本文深度對比原理、性能陷阱 (裝箱/拆箱)、線程安全誤區,並給出遷移指南。
| 特性 | Hashtable | ArrayList | Dictionary<TKey,TValue> | List<T> |
|---|---|---|---|---|
| 類型系統 | 非泛型 (`object` 鍵值) | 非泛型 (`object` 元素) | 泛型 (強類型) | 泛型 (強類型) |
| 底層結構 | 哈希表 (桶+鏈表/開放寻址) | 動態數組 | 哈希表 (優化) | 動態數組 |
| 鍵值/元素類型 | 任意 `object` | 任意 `object` | `TKey` / `TValue` | `T` |
| 值類型存儲 | **裝箱** | **裝箱** | **無裝箱** (結構體內聯) | **無裝箱** |
| 查找複雜度 | O(1) 平均 | O(n) 線性 | O(1) 平均 | O(n) 線性 (索引 O(1)) |
| 線程安全 | 僅 `Synchronized` 包裝器支持**單操作**原子性 | 同上 | 無內置線程安全 | 無內置線程安全 |
| 鍵為 null | 允許 (一個) | N/A | 允許 (若 `TKey` 為引用類型) | N/A |
| 鍵區分大小寫 | 默認區分 (`IEqualityComparer`) | N/A | 默認區分 | N/A |
using System.Diagnostics;
public static void BoxingBenchmark()
{
const int Count = 1_000_000;
var sw = Stopwatch.StartNew();
// ArrayList 存 int (裝箱)
var arrayList = new ArrayList();
for (int i = 0; i < Count; i++) arrayList.Add(i);
long alMem = GC.GetTotalMemory(true);
sw.Stop();
Console.WriteLine($"ArrayList Add: {sw.ElapsedMs}ms, Mem: {alMem/1024/1024}MB");
// 每次 Add 分配一個 Int32 對象在堆上
sw.Restart();
// List 存 int (無裝箱)
var list = new List(Count);
for (int i = 0; i < Count; i++) list.Add(i);
long listMem = GC.GetTotalMemory(true);
sw.Stop();
Console.WriteLine($"List Add: {sw.ElapsedMs}ms, Mem: {listMem/1024/1024}MB");
// 內存佔用約為 ArrayList 1/3,速度快 3-5 倍
}
結論:高頻值類型操作,**非泛型集合嚴重拖垮性能與內存**。這是棄用它們的首要原因。
var ht = new Hashtable();
// 添加
ht.Add("id", 1001); // 鍵值均為 object
ht["name"] = "Tom"; // 索引器覆蓋/添加
// 遍歷 - 必須用 DictionaryEntry (鍵值對結構體)
foreach (DictionaryEntry de in ht)
{
Console.WriteLine($"Key: {de.Key} (Type: {de.Key.GetType()}), Value: {de.Value} (Type: {de.Value.GetType()})");
// 強制轉型
if (de.Key is string key && de.Value is int val) { /* ... */ }
}
// 判斷鍵存在
bool has = ht.ContainsKey("id"); // 推薦
bool hasOld = ht.Contains("id"); // 同義,舊方法
// 刪除
ht.Remove("id");
ht.Clear();
// Hashtable.Synchronized 返回包裝器,僅保證單個方法原子
var syncHt = Hashtable.Synchronized(new Hashtable());
// 多線程下「檢查後操作」非原子 - 競態條件
// 線程 A // 線程 B
if (!syncHt.ContainsKey("k")) if (!syncHt.ContainsKey("k"))
syncHt.Add("k", 1); syncHt.Add("k", 1); // 可能拋異常或覆蓋
// 正確:自行加鎖
lock (syncHt.SyncRoot) // 使用包裝器暴露的 SyncRoot
{
if (!syncHt.ContainsKey("k")) syncHt.Add("k", 1);
}
// 現代替代:ConcurrentDictionary (無鎖高性能)
var al = new ArrayList { 1, "two", 3.0, true }; // 混合類型 - 惡夢開始
// 取值必須轉型
int a = (int)al[0]; // 運行時若類型不匹配拋 InvalidCastException
string b = (string)al[1];
// 遍歷
foreach (object obj in al) { /* 處理 object */ }
// 插入/刪除 (O(n) 移動元素)
al.Insert(1, "inserted");
al.RemoveAt(2);
al.Remove("two"); // 刪除首個匹配項
// 排序 (要求元素實現 IComparable 或傳 IComparer)
al.Sort(); // 混合類型拋異常
| 舊代碼模式 | 現代替代 | 關鍵修改點 |
|---|---|---|
| `Hashtable ht = new Hashtable();` | `var dict = new Dictionary | 指定鍵值類型;若值確異構用 `object` 或定義基類/接口 |
| `ht["key"] = value;` | `dict["key"] = value;` | 語法相同 |
| `foreach (DictionaryEntry de in ht)` | `foreach (var kvp in dict)` | `kvp.Key`、`kvp.Value` 強類型 |
| `ArrayList al = new ArrayList();` | `var list = new List | 指定元素類型 |
| `al.Add(item);` | `list.Add(item);` | 類型檢查在編譯期 |
| `(int)al[i]` | `list[i]` | 直接得 `int`,無轉型、無拆箱 |
| `Hashtable.Synchronized` | `ConcurrentDictionary` / `ImmutableDictionary` / 自行 `lock` | 更高性能、更清晰語義 |
**口訣**:「非泛型是遺產,泛型是當下,併發用 Concurrent,不可變用 Immutable,裝箱拆箱是大敵,類型安全靠編譯。」