必须同时重写Equals和GetHashCode,否则Dictionary、HashSet中无法正确查找对象;因哈希容器先用GetHashCode定位桶,再用Equals精确匹配,二者字段不一致会导致逻辑相等对象散列到不同桶而查找不到。

必须同时重写 Equals 和 GetHashCode,否则放进 Dictionary、HashSet 后可能查不到自己——这不是警告,是运行时真实故障。
为什么只重写 Equals 会让字典“失忆”
哈希容器(如 Dictionary<TKey, TValue>)查找 key 时分两步:先调 GetHashCode() 定位桶,再在桶内用 Equals() 精确比对。如果两个逻辑相等的对象返回不同哈希码,它们会被分到不同桶里,Equals() 根本不会被调用。
常见错误现象:
-
dict.Add(new Person(1, "A"), "value");成功,但dict.ContainsKey(new Person(1, "A"))返回false -
new HashSet<Person> { p1, p2 }包含两个“相同”对象,Distinct()不去重
根本原因:GetHashCode() 仍用默认实现(基于内存地址或字段反射),而 Equals() 已按业务逻辑判断相等。
Equals(object) 重写的最小安全写法
不处理 null、类型不匹配、装箱开销大,就等于没重写。推荐用 C# 7+ 的模式匹配写法,简洁且安全:
public override bool Equals(object obj)
{
if (obj is Person other)
{
return Id == other.Id
&& string.Equals(Name, other.Name, StringComparison.Ordinal)
&& BirthDate == other.BirthDate;
}
return false;
}关键点:
- 用
obj is Person other一次性完成非空 + 类型检查 + 变量声明,比obj == null || GetType() != obj.GetType()更可靠 - 引用类型字段(如
Name)必须用string.Equals(a, b, ...),避免a == b在 a 为 null 时抛异常 - 值类型字段(如
Id、BirthDate)直接用==即可 - 若类有基类且基类也重写了
Equals,末尾补上&& base.Equals(obj)
GetHashCode() 必须和 Equals 字段严格一致
哈希码不要求唯一,但必须保证:所有在 Equals 中参与比较的字段,都必须参与哈希计算;反之,没参与 Equals 的字段,绝不能出现在 GetHashCode 中。
public override int GetHashCode()
{
return (Id, Name, BirthDate).GetHashCode();
}注意事项:
- C# 9+ 直接用元组解构最安全,
(Id, Name, BirthDate).GetHashCode()自动处理 null 和值类型 - .NET Core 2.1+ 可用
HashCode.Combine(Id, Name, BirthDate),性能略优 - 绝对不要用可变字段(如后续会
person.Name = "B"修改的属性)——对象进HashSet后改名,哈希码变化,再也找不回来 - 避免手写
a.GetHashCode() ^ b.GetHashCode():异或对顺序敏感,且易因 0 值导致冲突升高
结构体(struct)要额外注意值语义陷阱
结构体默认已做字段级逐位比较,但手动重写时容易漏掉一致性约束:
- 即使不重写,
struct的默认Equals也会用反射遍历所有字段——性能差,且对Nullable<T>或嵌套struct行为不可控 - 重写后必须确保
GetHashCode仅依赖不可变字段;若字段是public且可写,等于主动埋雷 - 强烈建议实现
IEquatable<T>接口,避免装箱:public bool Equals(Person other) => Id == other.Id && ...
最容易被忽略的一点:如果你把自定义结构体用作 Dictionary<MyStruct, T> 的 key,又没重写 GetHashCode,它确实能跑,但哈希分布极差——所有实例可能挤进同一个桶,退化成链表查找。


















