C# 中的 `Hashtable`

今天我们来详细解释一下 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 通过以下两种主要方法之一来解决冲突:

  1. 拉链法 :这是最常用的方法。每个"桶"不再存储单个元素,而是存储一个链表(或其他数据结构)。当发生冲突时,新的键值对会被添加到对应桶的链表中。在查找时,会先定位到桶,然后在链表中进行线性搜索,使用 Equals 方法来比较键。
  2. 开放地址法:当发生冲突时,它会按照某种探测规律(如线性探测、二次探测)在数组中寻找下一个空闲的位置。

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 是非泛型的,它存在一些显著的缺点:

  1. 性能开销(装箱/拆箱) :当存储值类型(如 int, struct)时,会发生装箱 操作(将值类型转换为 object),在读取时会发生拆箱 (将 object 转换回值类型),这会影响性能。
  2. 类型不安全 :编译器无法检查类型,容易在运行时因类型转换错误而引发 InvalidCastException。
  3. 代码可读性差:需要频繁地进行显式类型转换。
推荐使用的现代替代品: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 主要用于维护旧的代码库。

相关推荐
天外飞雨道沧桑9 分钟前
我在浏览器里练了一个 792 参数的大模型,它学会了补全 JavaScript
开发语言·javascript·ai·ecmascript·ai编程
hhhjhl14 分钟前
Yup 跨端三方库仓颉适配全流程
java·开发语言
运维行者_26 分钟前
网络监控工具怎么选?从设备监控到智能运维的能力清单
运维·服务器·开发语言·网络·人工智能·支持向量机·php
“AI国潮设计-小江”1 小时前
【AIGC实战】Python+SDXL打造“潮汕英歌舞IP”万能立牌插件,批量生成“发财元宝蛋糕”视觉营销方案(附源码)
开发语言·人工智能·python·prompt·aigc
lhbweilai2 小时前
分析FreeRTOS内核对象API的源码实现(八)
c语言·开发语言·stm32·单片机
努力努力再努力wz2 小时前
【C++高阶系列】C++20 协程底层原理:函数为什么能暂停?从状态机到 Task,彻底串起挂起、恢复与协程调度
开发语言·c++·c++20·1024程序员节
咩咩啃树皮2 小时前
JavaScript 中 this 指向为什么总在变?
开发语言·前端·javascript
程序员清风2 小时前
算法题中的边界条件、测试与调试
开发语言·算法·kotlin
砚底藏山河2 小时前
python量化实操:冷热分层存储
java·开发语言·python·金融
十五年专注C++开发2 小时前
pybind11:一个跨C++和Python平台的编程库
开发语言·c++·python·pybind11