hashset去重不是替代list,而是解决查得快、去得稳、算得准的问题:它不保序、不支持索引、不能存null(除非t可空),但contains和add均为均摊o(1);需重写gethashcode与equals或用record确保去重正确;集合运算慎用原地方法避免意外修改。

直接说结论:用 HashSet<t></t> 去重,不是“替代 List”,而是解决「查得快、去得稳、算得准」的问题。它不保序、不支持索引、不能存 null(除非 T 是可空引用类型),但查 Contains 和加 Add 都是均摊 O(1)。
怎么初始化和添加元素?别忽略 Add() 的返回值
初始化很简单:var set = new HashSet<string>();</string>,或一次性从已有集合构造:new HashSet<int>(list)</int> —— 构造时就自动去重。
添加时重点在 Add() 的返回值:它返回 bool,不是 void。重复添加会返回 false,且集合不变。
- 错误写法:
set.Add(item); if (set.Contains(item)) { ... }—— 多一次哈希查找,纯属冗余 - 正确写法:
if (set.Add(item)) { ProcessNew(item); }—— 一行完成“尝试加入 + 确认是新值” - 注意:对引用类型(如
Person),必须确保GetHashCode()和Equals()逻辑一致;用record最省心
去重到底靠什么?Equals() 和 GetHashCode() 缺一不可
HashSet<t></t> 判断重复,不是靠 ==,也不是只看 Equals(),而是先比哈希码,再比相等性。两者不匹配,去重就会失效。
- 值类型(如
int、DateTime)默认行为可靠,不用改 - 引用类型(如自定义类)若没重写
GetHashCode()和Equals(),两个内容相同的对象会被视为不同元素 - 推荐用
record Person(string Name, int Age),编译器自动生成正确的哈希与比较逻辑 - 如果必须用 class,且无法修改源码,就传入
IEqualityComparer<t></t>实现,比如new HashSet<person>(new PersonComparer())</person>
交集、并集、差集怎么算?注意原集合是否被修改
HashSet<t></t> 自带原地集合运算方法,但它们会**直接修改调用方集合**:
-
set1.UnionWith(set2)→set1变成并集,set2不变 -
set1.IntersectWith(set2)→set1变成交集,set2不变 -
set1.ExceptWith(set2)→set1变成差集(set1 - set2) - 如果不想改原集合,用 LINQ 的
Intersect()、Union()等扩展方法(需using System.Linq;),它们返回新集合
注意:Intersect() 和 IntersectWith() 行为不同,一个新建、一个就地改——选错会导致后续逻辑读到意外状态。
什么时候不该用 HashSet<t></t>?三个典型误用场景
它高效,但不是万能胶。以下情况优先考虑其他结构:
- 需要按插入顺序遍历?→
HashSet不保证顺序(.NET 5+ 也不维护插入序),改用List<t></t>+HashSet<t></t>双存(主列表 + 哈希查重缓存) - 要通过下标访问(
list[i])?→HashSet没索引器,只能 foreach 或转成数组 - 数据量极小(List
.Contains() 开销差别几乎为零,没必要引入额外类型和内存开销
最容易被忽略的点是:null 值处理。对 HashSet<string></string>,set.Add(null) 合法;但对 HashSet<string></string>(可空引用类型),行为一致;而值类型如 HashSet<int></int> 无法存 null,编译都不过。










