广告位 · 页头(后台-模块管理-新建模块后替换此区域)

C#哈希表Hashtable怎么用?Hashtable和Dictionary有哪些区别?

Hashtable 是 .NET 1.0 时代就存在的键值集合类型,位于 System.Collections 命名空间。它与 Dictionary<TKey,TValue> 都能借助哈希算法实现接近 O(1) 的查找,但一个是非泛型,另一个是泛型,两者在用法、性能和异常行为上差别相当大。本文先介绍 Hashtable 的常用 API,再对比它与 Dictionary 的核心差异,最后给出实际的选型建议。

C#哈希表Hashtable怎么用?Hashtable和Dictionary有哪些区别?

一、Hashtable 的基本用法与内部原理

Hashtable 内部维护一个由哈希桶组成的数组,每个桶存放一个链表或者碰撞链。当你调用 Add 方法时,Hashtable 会先对键调用 GetHashCode 得到哈希码,再通过取模运算定位到具体的桶索引。如果该位置已经存在元素,就会发生哈希碰撞,新元素会被挂到该桶的链表尾部。因此,哈希函数的质量和桶的数量会直接影响查找效率。容量越大、负载因子越低,碰撞概率越小,但内存占用也越高。

创建 Hashtable 最简单的方式是直接使用无参构造函数,它会以默认容量和负载因子初始化集合。也可以调用 new Hashtable(int capacity, float loadFactor) 来显式指定初始容量和负载因子。当元素数量超过容量乘以负载因子时,Hashtable 会自动扩容并重新哈希所有元素,这个过程相对耗时。所以在知道大致数据规模时,建议提前设置合理的初始容量。

下面的代码演示了 Hashtable 的常见操作,包括添加、覆盖、读取、判断键是否存在、遍历以及删除。

using System;
using System.Collections;
class HashtableDemo
{
    static void Main()
    {
        // 默认构造函数,容量和负载因子使用默认值
        Hashtable table = new Hashtable();
        // 使用 Add 方法添加键值对
        table.Add("id", 1001);
        table.Add("name", "Alice");
        table.Add("active", true);
        // 索引器可以直接添加或覆盖已有键
        table["score"] = 95.5;
        table["id"] = 1002; // 覆盖已有键的值
        // 读取时需要进行显式类型转换
        int id = (int)table["id"];
        string name = (string)table["name"];
        Console.WriteLine($"ID={id}, Name={name}");
        // 判断键是否存在
        if (table.ContainsKey("score"))
        {
            Console.WriteLine("score 存在");
        }
        // 遍历所有 DictionaryEntry
        foreach (DictionaryEntry entry in table)
        {
            Console.WriteLine($"Key={entry.Key}, Value={entry.Value}");
        }
        table.Remove("active");
    }
}

可以看到,从 Hashtable 中取出值后通常需要强制类型转换。这是因为 Hashtable 把键和值都当作 object 类型存储。对于值类型数据,存入时会发生装箱,取出时发生拆箱。装箱和拆箱会在托管堆上分配对象并复制数据,频繁操作会带来可观的性能开销。此外,编译期无法检查类型是否匹配,如果将值转换为错误的类型,程序会抛出 InvalidCastException。

Hashtable 的键不能为 null,否则会立即抛出 ArgumentNullException。值可以为 null,但在遍历时需要注意判断。Hashtable 本身不是线程安全的,如果多个线程同时读写同一个实例,可能会造成数据损坏或无限循环。不过 .NET 提供了 Hashtable.Synchronized 静态方法,可以返回一个线程安全的包装实例。

二、Hashtable 与 Dictionary 的核心差异

两者最根本的区别在于泛型。Hashtable 是非泛型集合,键和值都是 object 类型;Dictionary<TKey,TValue> 是泛型集合,键和值的类型在声明时就固定下来。这个差异直接决定了类型安全、运行效率以及使用体验。使用 Dictionary 时,编译器会检查传入的键和值是否符合类型约束,错误的数据类型会在编译阶段暴露,而不是等到运行时才抛异常。

性能方面,Dictionary 通常明显优于 Hashtable。因为 Dictionary 不需要装箱和拆箱,对于值类型键值尤其明显。例如存储一百万个 int 类型的键值对,Hashtable 会为每个 int 进行装箱,托管堆上会多出一百万个对象,增加垃圾回收压力;而 Dictionary 直接使用原生 int,内存占用更小,访问速度更快。此外,Dictionary 还提供了 TryGetValue 方法,可以在一次查找中同时判断键是否存在并获取值,避免了 Hashtable 中先调用 ContainsKey 再取值的两次哈希查找。

特性HashtableDictionary
泛型支持非泛型,键值均为 object泛型,编译期确定类型
类型安全弱类型,需强制转换强类型,编译器检查
装箱拆箱值类型会发生装箱拆箱无装箱拆箱
null 键不支持,抛 ArgumentNullException引用类型键可为 null,但需谨慎
null 值允许值类型不允许,引用类型允许
线程安全可通过 Synchronized 包装非线程安全,需 ConcurrentDictionary
遍历元素DictionaryEntryKeyValuePair<TKey,TValue>

下面的 Dictionary 示例展示了与 Hashtable 相同的操作,但代码更简洁,也不需要手动类型转换。

using System;
using System.Collections.Generic;
class DictionaryDemo
{
    static void Main()
    {
        Dictionary<string, int> scores = new Dictionary<string, int>();
        scores.Add("Alice", 90);
        scores["Bob"] = 85; // 索引器添加
        scores["Alice"] = 95; // 覆盖已有键
        // TryGetValue 避免两次查找
        if (scores.TryGetValue("Alice", out int aliceScore))
        {
            Console.WriteLine($"Alice score: {aliceScore}");
        }
        foreach (KeyValuePair<string, int> kvp in scores)
        {
            Console.WriteLine($"{kvp.Key} -> {kvp.Value}");
        }
    }
}

关于线程安全,经常有一个误区:很多人认为 Hashtable 本身就是线程安全的。其实普通的 Hashtable 实例并不是线程安全的,只有通过 Hashtable.Synchronized 方法创建出来的包装实例才提供有限的线程安全。即便如此,对于“先检查再新增”这样的复合操作,仍然需要使用 SyncRoot 显式加锁。Dictionary 则完全不提供线程安全机制,在多线程场景下应该使用 ConcurrentDictionary<TKey,TValue>,它通过细粒度锁和无锁算法实现了更好的并发性能。

另一个容易忽视的差异是 null 键。Hashtable 遇到 null 键会直接抛出 ArgumentNullException,而 Dictionary 允许引用类型的键为 null。不过允许 null 键会带来额外的判断逻辑,因为 null 无法调用 GetHashCode,Dictionary 内部需要特殊处理。实际开发中,如果键可能为 null,需要确认业务逻辑是否合理,避免因 null 键导致意外行为。

三、实际开发中的选型建议与常见误区

如果你正在开发新的 .NET 项目,几乎没有任何理由选择 Hashtable。Dictionary 在类型安全、性能、可读性方面都全面占优,而且与 LINQ、泛型方法等现代 C# 特性配合得更自然。只有一种情况需要继续使用 Hashtable:维护遗留代码或与那些只接受非泛型集合的旧接口交互。比如某些早期版本的第三方库或者 .NET Framework 1.x 的 API 仍在使用 Hashtable,此时为了兼容,只能继续沿用。

对于并发场景,选择 ConcurrentDictionary<TKey,TValue> 会比手动给 Hashtable 或 Dictionary 加锁更高效、更安全。它内部采用分段锁和原子操作,在读多写少的场景下吞吐量通常高于使用 lock 的普通集合。如果只是读多写少且数据量不大,也可以考虑使用 ReadOnlyDictionary<TKey,TValue> 来暴露只读视图。

另一个常见误区是认为 Hashtable 因为“老”所以“稳定”,或者因为“非泛型”所以“灵活”。实际上,非泛型带来的类型模糊和装箱开销只会让代码更难维护、更容易出错。还有人在 Hashtable 中混用不同类型的键,比如同时插入字符串键和 int 键。虽然这在语法上合法,但哈希排序和查找会变得不可预测,应该尽量避免。Dictionary 也会遇到类似问题,但由于泛型约束,至少键的类型是统一的。

最后提醒一点,无论使用 Hashtable 还是 Dictionary,都应该注意初始容量设置。如果预知数据量较大,在构造函数中传入一个合理的容量值,可以减少扩容时的哈希重建次数,显著提升写入性能。Dictionary 也提供了 new Dictionary<TKey,TValue>(int capacity) 构造函数,它的内部哈希表大小会调整为不小于容量值的最小质数或者相关阈值。这个细节在高频写入场景下非常实用。

总体来看,Hashtable 更适合作为历史遗留代码中的兼容方案,而 Dictionary 才是现代 C# 开发中键值集合的首选。理解二者的差异,能够帮助你在项目中选择正确的数据结构,避免不必要的性能损失和类型安全问题。

广告位 · 文章底部(后台-模块管理替换此区域)