初始化 HashSet 必须指定泛型类型参数,如 new HashSet();Add() 是唯一添加方式且返回是否新增成功;Contains() 判断存在性,时间复杂度 O(1);遍历无序,去重依赖 GetHashCode() 和 Equals()。

初始化 HashSet 时别漏掉泛型类型参数
直接写 new HashSet() 会编译失败,C# 的 HashSet<t></t> 是泛型类,必须指定元素类型。常见错误是照着数组或 List 的写法省略类型,比如写成 new HashSet() 而不是 new HashSet<string>()</string> 或 new HashSet<int>()</int>。
正确写法有几种常用形式:
-
var set = new HashSet<string>();</string>—— 最直白,显式声明类型 -
var set = new HashSet<string> { "a", "b", "c" }; </string>—— 初始化器语法,内部自动调用Add -
var set = new HashSet<string>(StringComparer.OrdinalIgnoreCase);</string>—— 指定比较器,影响重复判断逻辑(比如忽略大小写) -
var set = new HashSet<person>(new PersonEqualityComparer());</person>—— 自定义类型需传入实现IEqualityComparer<t></t>的比较器,否则默认用引用比较
往 HashSet 里加元素:Add() 是唯一推荐方式
Add() 返回 bool,true 表示新增成功,false 表示该值已存在。这点和 List.Add() 不同,它不抛异常也不静默覆盖。
别用 set[i] = x 或 set.AddRange(...) —— 这些根本不存在。HashSet 没有索引器,也没有内置批量添加方法(.NET 6+ 才有 UnionWith 类似功能)。
常见误操作:
- 误以为
Add()会更新已有元素 → 实际上它只管“有没有”,不改内容 - 对自定义对象不提供
GetHashCode()和Equals()→ 导致重复项无法识别,集合行为异常 - 在循环中反复调用
Add()却忽略返回值 → 失去去重意图的反馈依据
判断是否存在:Contains() 比 TryGetValue() 更合适
HashSet<t></t> 没有 TryGetValue() 方法(那是 Dictionary 的),只有 Contains()。它时间复杂度接近 O(1),比 List.Contains() 快得多。
注意点:
-
Contains()的行为严格依赖你传入的比较器。比如用StringComparer.OrdinalIgnoreCase初始化的集合,set.Contains("HELLO")会返回true即使存的是"hello" - 对 null 值要小心:如果泛型类型是引用类型且未指定允许 null 的比较器,
Contains(null)可能返回false或抛NullReferenceException(取决于比较器实现) - 别用
set.ToList().Contains(x)—— 完全绕过哈希优势,退化为线性查找
遍历和转换:foreach 安全,ToArray() 会复制但无序
HashSet<t></t> 不保证插入顺序,也不保证遍历顺序。虽然当前实现下多次遍历结果可能一致,但这不是契约,不能依赖。
常用操作:
-
foreach (var item in set) { ... }—— 安全、高效,底层迭代器直接访问桶结构 -
set.ToArray()—— 创建新数组,内容是当前所有元素,但顺序不确定;如果后续要排序,应显式调用OrderBy -
set.ToList()—— 同样无序,且多一次装箱/复制开销,除非真需要List特性,否则没必要 -
set.Count是 O(1),可放心用于条件判断,不用先ToArray().Length
最易被忽略的一点:HashSet 的「去重」完全由 GetHashCode() 和 Equals() 驱动,不是靠内存地址或 JSON 字符串对比。哪怕两个对象字段一模一样,只要没重写这两个方法,它们就被视为不同元素。


















