HashSet去重最快且支持高效集合运算,但需注意哈希正确性、无序性、线程安全及null处理;与List用途不同,不可替代。

怎么用 HashSet 去重
直接 new 一个 HashSet,把重复数据丢进去,它自动去重——这是最常用也最省事的去重方式。它底层用哈希表实现,插入和查找都是 O(1) 平均时间复杂度,比 List 调用 Distinct() 或手动遍历查重快得多。
常见错误是以为构造函数传入 List 就完事了,其实得确认元素类型支持合理哈希(比如自定义类没重写 GetHashCode() 和 Equals(),就会去重失效)。
- 基础用法:
var set = new HashSet(listWithDuplicates); - 字符串去重注意大小写:
new HashSet(StringComparer.OrdinalIgnoreCase) - 自定义类型必须重写
GetHashCode()和Equals(),否则两个逻辑相等的对象可能被当成不同元素存入
HashSet 求交集为什么比 List 快
因为 HashSet 的 IntersectWith() 是拿一个集合遍历,对每个元素在另一个集合里做哈希查找,平均 O(n);而 List 做交集通常要嵌套遍历,是 O(m×n)。
但注意:IntersectWith() 是原地修改调用方集合,不是返回新集合。如果不想改原集合,得先 new HashSet 再调用。
- 正确求交(不改原集合):
var result = new HashSet(setA); result.IntersectWith(setB); - 错误写法:
setA.IntersectWith(setB)—— 这会把setA变成交集结果,原数据丢了 - 如果需要 LINQ 风格返回新集合,用
setA.Intersect(setB).ToHashSet()(.NET 6+),别用ToList().Intersect(),那又退化回慢操作
HashSet 和 List 到底该选哪个
看你要不要「顺序」和「索引访问」。要,选 List;不要,且频繁查重、增删、求集合运算,选 HashSet。
它们根本不是替代关系,而是用途错位:一个管「有序序列」,一个管「唯一成员集合」。硬拿 HashSet 存日志并按添加顺序遍历?做不到——它不保证插入顺序(.NET 5+ 的 HashSet 也不维护插入序)。
- 查是否存在某元素:
set.Contains(x)比list.Contains(x)稳定快,尤其数据量大时 - 需要下标访问或保持插入顺序:只能用
List,或者组合用(比如用HashSet做查重缓存,主结构还是List) - 内存占用:
HashSet比List略高(要存哈希桶、空闲槽位),小数据量差异可忽略
容易被忽略的坑:null、引用类型、线程安全
HashSet 允许存 null(当 T 是引用类型或可空值类型),但如果你用的是带比较器的构造(比如 StringComparer.OrdinalIgnoreCase),null 传进去会抛 ArgumentNullException。
还有人把实体对象直接塞进 HashSet,结果发现去重失败——因为默认用引用比较,两个属性一模一样的对象仍是不同元素。这不是 bug,是设计如此。
- 含 null 的字符串集合,别用
StringComparer构造,改用默认构造 + 自定义IEqualityComparer处理 null - 实体类去重必须提供
IEqualityComparer,或确保类已重写GetHashCode()/Equals() -
HashSet不是线程安全的,多线程读写必须加锁,或改用ConcurrentHashSet(需 NuGet 包System.Collections.Concurrent,不是 BCL 自带)
哈希集合的边界感很清晰:它只负责“有没有”,不负责“第几个”“谁在前”。一旦混淆这个前提,后面所有操作都会绕弯路。










