HashSet 比 List.Contains 快因底层为哈希表,平均查找 O(1),而 List.Contains 是线性扫描最坏 O(n);十万数据查询前者微秒级、后者几毫秒,循环中使用会导致 n² 复杂度。

HashSet 为什么比 List.Contains 快得多
因为 HashSet<t></t> 底层是哈希表,平均查找时间复杂度是 O(1);而 List<t>.Contains</t> 是线性扫描,最坏 O(n)。十万条数据里查一个值,前者微秒级,后者可能要几毫秒——尤其在循环里反复调用时,性能差距会指数级放大。
常见错误现象:foreach (var item in list) { if (list.Contains(item)) {...} } 这种写法在去重或过滤时非常典型,但实际做了 n² 次遍历。
- 适用场景:需要频繁判断“某个值是否存在”,比如去重、白名单校验、跳过已处理 ID
- 注意
T必须正确实现GetHashCode()和Equals();自定义类不重写这两个方法,会导致去重失效 - 字符串、数字、枚举等内置类型默认支持,可直接用
用 HashSet 去重的三种典型写法及取舍
不是所有“去重”都该用 HashSet<t></t> 构造器一次性初始化——得看你是要结果集合,还是要边遍历边判断。
-
new HashSet<t>(sourceList)</t>:最常用,适合一次性把源集合去重转成新集合,返回的是可枚举的HashSet<t></t>实例 -
sourceList.Distinct().ToList():语义清晰,但底层仍会新建HashSet<t></t>,额外多一次遍历和内存分配,小数据无感,大数据略亏 - 边遍历边判断:
var seen = new HashSet<int>(); foreach (var id in rawIds) { if (!seen.Add(id)) continue; // Add 返回 false 表示已存在,天然去重 Process(id); }适合流式处理、避免全量加载,且不额外建新集合
Contains 和 Add 的陷阱:别忽略返回值含义
HashSet<t>.Add()</t> 不只是“加进去”,它返回 bool:成功加入返回 true,重复值返回 false。很多人只调用不判断,等于浪费了它的原子性优势。
- 错误写法:
seen.Add(item); if (seen.Contains(item)) { ... }——多一次哈希查找,完全没必要 - 正确写法:
if (seen.Add(item)) { Process(item); },一行完成“尝试加入 + 判断是否新值” - 注意:如果
T是可变引用类型(比如没设readonly的 struct 或字段可改的 class),后续修改对象内容可能导致哈希码变化,再查就找不到了——HashSet不会自动调整内部结构
内存与线程安全:别在高并发下直接共享 HashSet
HashSet<t></t> 本身不是线程安全的。多个线程同时调用 Add 或 Contains,可能抛出 InvalidOperationException 或静默出错(比如漏掉某些去重)。
- 简单方案:用
lock包裹关键操作,但会串行化,吞吐下降 - 推荐替代:
ConcurrentDictionary<t byte></t>(键存值,值用default(byte)占位),它线程安全且查找性能接近HashSet - 或者直接用
System.Collections.Concurrent.ConcurrentHashSet<t></t>(.NET 6+),但要注意它不包含在 .NET Standard 2.0 中,跨平台项目需确认目标框架
真正容易被忽略的是:哪怕你只读不写,只要其他线程在写,也必须同步——Contains 在写操作进行中可能看到不一致的内部状态。










