必须同时重写gethashcode和equals,因为默认object.gethashcode基于引用地址,导致相同逻辑内容的对象哈希值不同,dictionary等集合无法正确查找;若只重写equals不重写gethashcode,会违反哈希契约,使相等对象散列到不同桶中而丢失查找能力。

重写 GetHashCode 不是为了“让哈希值看起来更随机”,而是确保相同逻辑意义的键,返回相同哈希值;且不同键尽量分散——否则 Dictionary 查找会退化成链表遍历。
为什么自定义类作键必须重写 GetHashCode 和 Equals
默认的 Object.GetHashCode() 基于引用地址,两个内容完全相同的实例(比如两个 new Person("Alice", 30))哈希值大概率不同。这会导致:dict.ContainsKey(new Person("Alice", 30)) 返回 false,即使该键已存在。
更危险的是:如果只重写 Equals 而不重写 GetHashCode,.NET 运行时可能把相等对象散列到不同桶里,Dictionary 根本找不到它们——违反哈希契约,行为未定义。
- 必须同时重写两个方法,且
GetHashCode的结果仅依赖Equals中用到的字段 - 所有参与计算的字段必须是不可变的(构造后不修改),否则键插入后被改值,哈希桶位置就错了
- 推荐直接用 C# 9+ 的
record或元组,编译器自动合成正确实现:(name, age).GetHashCode()
GetHashCode 实现中常见的错误写法
以下写法看似合理,实则破坏哈希分布或稳定性:
- 用
DateTime.Now.GetHashCode()或Guid.NewGuid().GetHashCode()当哈希因子 —— 每次调用结果不同,键一放进字典就“消失” - 对字符串字段直接拼接再调用
.GetHashCode():(firstName + lastName).GetHashCode()——"AB" + "C"和"A" + "BC"结果相同,冲突率飙升 - 用
float或double字段参与计算 —— 浮点精度误差导致Equals为true但GetHashCode不同 - 忽略
null情况:name.GetHashCode()在name == null时抛NullReferenceException
正确做法示例(C# 7+):
public override int GetHashCode()
{
return HashCode.Combine(name?.GetHashCode() ?? 0, age, departmentId);
}
哈希值分布不均如何快速验证
你无法单看代码判断哈希是否“够好”,但可以低成本观测实际效果:
- 插入大量测试数据后,检查
dict.Count / dict.Capacity—— 若长期 > 0.7,说明负载过高,可能源于哈希聚集 - 用调试器观察
dict.buckets数组:若大量桶值为 -1(空),而少数桶指向很长的entries链表(next链深度 > 3),就是典型冲突集中 - 临时加日志:在
GetHashCode中记录返回值分布(如按区间计数),看是否集中在某几个数值段
注意:Dictionary 内部用 hashCode & 0x7FFFFFFF 取非负值,再对桶长取模,所以负哈希值本身不是问题;但若大量键的 GetHashCode() 模某个小质数后余数相同(比如都 % 7 == 2),就会挤进同一个桶。
什么时候该放弃自定义 GetHashCode?
不是所有场景都值得手写哈希逻辑:
- 键类型简单且固定(如
int、string、Guid)—— .NET 默认实现已足够好,别动 - 键含复杂嵌套或集合字段(如
List<int></int>)——GetHashCode难写易错,建议改用唯一 ID 字段作键,或转用SortedDictionary - 业务上“相等”定义频繁变更(比如有时忽略大小写,有时区分)—— 哈希逻辑会随语义漂移,极易出错,应封装为独立
IEqualityComparer<t></t>实现传入字典构造函数
真正棘手的从来不是“怎么写”,而是“哪些字段该参与哈希”——这个决策一旦定下,就等于锁死了键的语义边界。改它,往往意味着整个字典缓存要失效重刷。











