今天我们来详细解释一下 C# 中的 Hashtable。
1. 什么是 Hashtable?
Hashtable 是 .NET Framework 中的一个集合类,位于 System.Collections 命名空间。它代表了一个键值对的集合,这些键值对根据键的哈希代码进行组织。
核心特性:
- 键值对存储 :每个元素都是一个
DictionaryEntry对象,包含一个键(Key)和一个值(Value)。 - 哈希算法:通过键的哈希码来快速定位数据,从而实现高效的检索。
- 非泛型 :
Hashtable是在 .NET 1.0 时期引入的非泛型集合 。这意味着它存储的键和值都是object类型。 - 动态扩容:当元素数量达到某个阈值时,它会自动增加容量(即内部数组的大小)。
2. 核心工作原理
理解 Hashtable 的关键在于理解其哈希机制 和冲突解决。
a. 哈希函数
当你添加一个键值对时,Hashtable 会调用键对象的 GetHashCode() 方法来计算其哈希码。这个哈希码是一个数字,用于确定该键值对在 Hashtable 内部数组(通常称为"桶"或"buckets")中的初始存储位置。
GetHashCode() 方法需要满足以下条件,才能保证 Hashtable 高效工作:
- 一致性 :如果两个对象相等(根据
Equals方法),那么它们必须返回相同的哈希码。 - 高效性:计算应该非常快。
- 分布均匀:不同的对象应尽可能产生不同的哈希码,以减少冲突。
b. 处理哈希冲突
不同的键有可能计算出相同的哈希码,或者不同的哈希码被映射到同一个内部数组索引上,这种情况称为哈希冲突 。Hashtable 通过以下两种主要方法之一来解决冲突:
- 拉链法 :这是最常用的方法。每个"桶"不再存储单个元素,而是存储一个链表(或其他数据结构)。当发生冲突时,新的键值对会被添加到对应桶的链表中。在查找时,会先定位到桶,然后在链表中进行线性搜索,使用
Equals方法来比较键。 - 开放地址法:当发生冲突时,它会按照某种探测规律(如线性探测、二次探测)在数组中寻找下一个空闲的位置。
3. 主要特性
- 键必须是唯一的 :尝试添加具有相同键的元素会抛出
ArgumentException。 - 键不能为
null:尝试使用null作为键会抛出ArgumentNullException。 - 值可以为
null:允许将null作为值存储。 - 元素是无序的:不能保证键值对的遍历顺序与插入顺序相同。顺序可能会因扩容和重新哈希而改变。
- 线程安全 :
Hashtable通过Synchronized方法提供了一种线程安全的包装器。但在现代编程中,更推荐使用ConcurrentDictionary。
4. 基本用法(代码示例)
csharp
using System;
using System.Collections; // 必须引入此命名空间
class Program
{
static void Main()
{
// 1. 创建 Hashtable
Hashtable myHashtable = new Hashtable();
// 2. 添加元素
myHashtable.Add("name", "Alice");
myHashtable.Add("age", 30);
myHashtable.Add(1, "Number One"); // 键可以是不同类型,但不推荐
myHashtable["city"] = "New York"; // 使用索引器添加/修改
// 3. 访问元素
string name = (string)myHashtable["name"]; // 需要显式类型转换
Console.WriteLine($"Name: {name}"); // 输出:Name: Alice
// 使用索引器修改值
myHashtable["age"] = 31;
// 4. 检查键是否存在
if (myHashtable.ContainsKey("city"))
{
Console.WriteLine($"City: {myHashtable["city"]}");
}
// 5. 遍历 Hashtable
Console.WriteLine("\n--- 遍历所有键值对 ---");
foreach (DictionaryEntry de in myHashtable)
{
Console.WriteLine($"Key: {de.Key}, Value: {de.Value}, Type: {de.Value.GetType()}");
}
Console.WriteLine("\n--- 单独遍历所有键 ---");
foreach (var key in myHashtable.Keys)
{
Console.WriteLine($"Key: {key}");
}
Console.WriteLine("\n--- 单独遍历所有值 ---");
foreach (var value in myHashtable.Values)
{
Console.WriteLine($"Value: {value}");
}
// 6. 移除元素
myHashtable.Remove("age"); // 移除键为 "age" 的项
// 7. 清空
// myHashtable.Clear();
}
}
输出示例(顺序可能不同):
Name: Alice
City: New York
--- 遍历所有键值对 ---
Key: name, Value: Alice, Type: System.String
Key: city, Value: New York, Type: System.String
Key: 1, Value: Number One, Type: System.String
--- 单独遍历所有键 ---
Key: name
Key: city
Key: 1
--- 单独遍历所有值 ---
Value: Alice
Value: New York
Value: Number One
5. Hashtable 的缺点和现代替代品:Dictionary<TKey, TValue>
由于 Hashtable 是非泛型的,它存在一些显著的缺点:
- 性能开销(装箱/拆箱) :当存储值类型(如
int,struct)时,会发生装箱 操作(将值类型转换为object),在读取时会发生拆箱 (将object转换回值类型),这会影响性能。 - 类型不安全 :编译器无法检查类型,容易在运行时因类型转换错误而引发
InvalidCastException。 - 代码可读性差:需要频繁地进行显式类型转换。
推荐使用的现代替代品:Dictionary<TKey, TValue>
Dictionary<TKey, TValue> 位于 System.Collections.Generic 命名空间中,是 Hashtable 的泛型版本,解决了上述所有问题。
代码对比:
csharp
using System.Collections.Generic;
// 使用 Hashtable (老方法,不推荐)
Hashtable oldStyle = new Hashtable();
oldStyle.Add("age", 25);
int ageOld = (int)oldStyle["age"]; // 需要拆箱,可能 InvalidCastException
// 使用 Dictionary (现代方法,推荐)
Dictionary<string, int> modernDict = new Dictionary<string, int>();
modernDict.Add("age", 25);
int ageModern = modernDict["age"]; // 无需转换,类型安全,性能更好
Dictionary<TKey, TValue> 的优势:
- 类型安全:在编译时即可检查类型。
- 性能更好:避免了装箱和拆箱。
- 代码更清晰:无需显式类型转换。
6. 总结
| 特性 | Hashtable | Dictionary<TKey, TValue> |
|---|---|---|
| 命名空间 | System.Collections |
System.Collections.Generic |
| 类型 | 非泛型 | 泛型 |
| 性能 | 较低(存在装箱/拆箱) | 高 |
| 类型安全 | 否(运行时检查) | 是(编译时检查) |
| 键/值是否可为null | 键不能为null,值可以为null | 取决于泛型类型参数(例如 string 可为null,int 不能) |
| 推荐使用 | 遗留代码或需要与 .NET 1.x 兼容时 | 所有新项目 |
结论:
虽然理解 Hashtable 的工作原理对于学习数据结构和哈希概念非常重要,但在实际的 C# 开发中,你应该优先使用 Dictionary<TKey, TValue> 。Hashtable 主要用于维护旧的代码库。