C#中object.Equals默认比较引用而非内容,需重写Equals和GetHashCode以支持值相等;struct默认按字段值比较;record简化实现但需注意属性类型是否重写Equals;哈希码须基于不可变字段且与Equals逻辑一致。

Equals 方法不比较内容?默认只看引用
在 C# 里,object.Equals(a, b) 默认行为是引用相等——只要两个变量指向堆上同一块内存,就返回 true。哪怕两个 Person 对象所有字段值都一样,只要不是同一个实例,就判为不等。
常见错误现象:new Person("Alice", 30).Equals(new Person("Alice", 30)) 返回 false,让人误以为“没写对”,其实是根本没重写。
- 必须同时重写
Equals(object)和GetHashCode(),否则字典、哈希集合会出问题 - 如果类型是
struct,默认已按字段值比较,但建议仍显式实现以明确语义 - 重写
Equals时,别忘了先做null检查和类型检查,否则可能抛NullReferenceException或静默返回false
重写 Equals 的标准写法(含类型安全)
最稳妥的模式是:先判断是否为 null,再用 as 转型并判空,避免 is + 强转带来的两次类型检查。
public override bool Equals(object obj)
{
var other = obj as Person;
if (other is null) return false;
return Name == other.Name && Age == other.Age;
}注意点:
- 别用
obj is Person other后直接访问other——当obj是子类实例时,可能绕过你本意的比较逻辑 - 字段比较要用
==还是Equals?对string推荐用string.Equals(a, b, StringComparison.Ordinal),避免空引用;对数值、枚举等值类型,==更快也更清晰 - 如果类有继承关系,且子类可能参与比较,得考虑是否调用
base.Equals,并确保父类的Equals实现是可靠的
GetHashCode 必须和 Equals 保持一致
GetHashCode 不是用来“生成唯一 ID”的,而是为了在哈希容器中快速分桶。只要 Equals 返回 true,两个对象的 GetHashCode 就必须相同;反过来不强制要求。
典型翻车现场:Dictionary<Person, string> 里加了一个 Person,之后改了它的 Name 字段,再用原对象去查——找不到。因为哈希码变了,桶位置已失效。
- 哈希码应仅基于
Equals中用到的、不可变(或至少在字典生命周期内不变)的字段计算 - 推荐用
HashCode.Combine(field1, field2)(.NET Core 2.1+),它比手写xor或乘加更均衡、不易冲突 - 如果用了可变字段(比如临时缓存字段),要么不参与哈希计算,要么文档里明确警告“修改后不得用于哈希容器”
record 类型能省事,但不等于不用思考
.NET 5+ 的 record 默认实现了基于属性的 Equals 和 GetHashCode,看起来一劳永逸。但它只对 record 自身声明的 init 或 get-only 属性生效。
容易被忽略的细节:
- 如果属性类型是普通 class(比如
Address),而它没重写Equals,那整个 record 的相等性还是退化为引用比较 -
record struct行为不同:它默认按所有字段位比较,包括引用类型字段的地址值,这通常不是你想要的 - 用
with表达式创建副本时,如果内部字段是可变引用类型,副本和原对象仍共享同一实例——相等性判断看似成立,实际数据可能被意外修改
真正麻烦的从来不是语法怎么写,而是想清楚“什么才算相等”:是字段值完全一致?忽略大小写?容忍浮点误差?还是业务上认为 ID 相同即相等?这些决策一旦定下,Equals 和 GetHashCode 就得严格对齐,而且很难再改。






