HashSet 初始化和基本添加操作
直接用
new HashSet<t>()</t>创建即可,不需要手动去重逻辑。它内部基于哈希表实现,插入、查找平均时间复杂度是 O(1)。
常见错误:用
List<t></t>+
Contains手动判断再添加,性能差且易漏判(尤其并发场景)。 添加重复元素时,
Add()返回
false,原集合不变 支持泛型约束,比如
HashSet<string></string>、
HashSet<int></int>,不支持裸
HashSet若存自定义类,必须重写
GetHashCode()和
Equals(),否则默认引用比较,相同内容对象仍被当作不同元素
如何正确处理自定义类型去重
默认情况下,
HashSet<person></person>把两个字段完全相同的
Person实例视为不同元素——因为没指定比较逻辑。
解决方法是传入实现了
IEqualityComparer<t></t>的比较器,或让类型自身实现该接口。 推荐方式:在构造时传入比较器,如
new HashSet<person>(new PersonComparer())</person>更简洁做法:让
Person类实现
IEquatable<person></person>并重写
GetHashCode()和
Equals(Person)注意:仅重写
Equals(object)不够,
HashSet优先调用泛型
Equals(T)
HashSet 与 List/Distinct() 的实际取舍
不是所有“去重需求”都该用
HashSet。它无序、不可索引、不保证插入顺序(.NET 5+ 保持插入顺序,但属实现细节,不应依赖)。
典型误用场景:先去重再按原顺序遍历,却选了
HashSet导致顺序错乱。 需要保留首次出现顺序 → 用
List<t>.Distinct()</t>(配合
IEqualityComparer),或手写带字典缓存的去重循环 高频查询/插入且不要求顺序 →
HashSet是最优解 要同时支持快速查重 + 按索引访问 → 考虑
List<t></t>+
HashSet<t></t>双结构维护(空间换时间)
线程安全与常见陷阱
HashSet<t></t>本身不是线程安全的。多线程同时调用
Add或
Contains可能导致异常或数据损坏。 简单场景:用
lock包裹操作块,注意锁粒度别太大 高并发场景:改用
ConcurrentHashSet<t></t>(需 NuGet 安装
System.Collections.Concurrent扩展包),或
ConcurrentDictionary<t object></t>模拟(key 存元素,value 固定为
null) 陷阱:用
foreach遍历时修改集合(如边遍历边
Remove)会抛出
InvalidOperationException真正容易被忽略的是:**.NET 中没有内置的线程安全版
HashSet,也没有
ConcurrentHashSet类型(除非你装了第三方扩展包)。很多人以为
ConcurrentDictionary有对应集合,其实没有——得自己封装或接受折中方案。**
