hashset 专为高效查重、去重、集合运算设计,非list替代品;正确使用add()返回值判断新旧元素,自定义类型需重写gethashcode()和equals(),原地操作方法会修改原集合,保序或并发场景应选其他结构。

HashSet<t></t> 不是用来“替代 List<t></t>”的,它是专为「查得快、去得稳、算得准」设计的数据结构。如果你只是要一个去重后的结果列表,Distinct() 更直白;但如果你在循环中反复判断“这个值来过没?”,那 HashSet<t></t> 是唯一合理选择。
HashSet.Add() 返回值必须用,否则等于白写
很多人写成:set.Add(item); if (set.Contains(item)) { ... }——这多一次哈希查找,纯属浪费 CPU。Add 操作本身已经完成了“计算哈希 → 查桶 → 判断相等 → 决定是否插入”的全过程,返回 true 就代表是新值,false 就是重复项。
- 正确写法:
if (seen.Add(item)) { Process(item); } - 错误写法:
seen.Add(item); if (seen.Contains(item)) { ... }(冗余且慢) - 对引用类型(如
Person),若没重写GetHashCode()和Equals(),Add()会把两个内容相同的对象当成不同元素——返回永远是true,去重彻底失效 - 推荐直接用
record Person(string Name, int Age),编译器自动生成匹配的哈希与比较逻辑
交集/并集/差集别乱调原地方法,改的是你手里的集合
IntersectWith()、UnionWith()、ExceptWith() 全部是 in-place 操作:调用方集合被直接修改,传入的参数集合不变。这不是 LINQ 那种“返回新集合”的风格。
-
set1.IntersectWith(set2)→set1变成交集,set2毫发无损 - 想保留原集合?改用 LINQ:
set1.Intersect(set2).ToHashSet()(需using System.Linq;) - 注意
Intersect()(LINQ)和IntersectWith()(原生)名字只差一个With,行为却天差地别,拼错就踩坑 - 所有原地运算方法都不返回新集合,也没有
readonly保护,调完立刻影响后续逻辑
什么时候不该用 HashSet?三个硬伤绕不开
它快,但有代价。以下场景优先换结构:
- 需要按插入顺序遍历?→
HashSet<t></t>不保序,考虑Dictionary<tkey tvalue></tkey>(用 value 存数据,key 当索引)或第三方LinkedHashSet(非 .NET 原生) - 元素可能被中途修改(比如 class 字段可变)?→ 哈希码变化后,
Contains()找不到,Remove()失效,Add()还可能误判为新值。要么加readonly,要么改用不可变类型 - 高并发读写?→
HashSet<t></t>非线程安全。不要裸奔共享,加lock会串行化,真要并发请用ConcurrentDictionary<tkey bool></tkey>模拟(key 当元素,value 固定为true)
最常被忽略的一点:HashSet<t></t> 的去重能力完全依赖 GetHashCode() 和 Equals() 的一致性。值类型默认靠谱,但只要涉及自定义类,就必须确认这两者是否同步更新——哪怕只改了一个字段的比较逻辑,漏掉哈希重算,整个去重就崩了。










