Equals()默认比较引用,自定义类型需重写Equals和GetHashCode;record类型自动实现值相等;重写时须处理null、类型检查,并确保GetHashCode与Equals逻辑一致。

Equals() 方法默认只比较引用,不是值
直接调用 object.Equals(a, b) 或 a.Equals(b) 时,如果类型没重写 Equals,它就走 ReferenceEquals —— 也就是看是不是同一个内存地址。哪怕两个 Person 对象所有字段都一样,只要不是同一个实例,就返回 false。
常见错误现象:new Person("Alice", 30).Equals(new Person("Alice", 30)) 返回 false,让人误以为“值相等逻辑失效了”,其实是根本没进值比较。
- 只有
string、int等内置值类型和部分 FCL 类型(如DateTime)才默认实现了值语义的Equals - 自定义类/结构体必须手动重写
Equals(object)和GetHashCode(),否则哈希容器(如Dictionary、HashSet)会出问题 - 重写时别忘了处理
null参数和类型检查,否则运行时报NullReferenceException或InvalidCastException
重写 Equals 时必须同步重写 GetHashCode
GetHashCode() 不是可选项,它是契约的一部分:如果两个对象 Equals 返回 true,它们的 GetHashCode() 必须相同;反之不成立。忽略这点,放进 Dictionary<TKey, TValue> 或用作 HashSet<T> 元素时,对象可能“消失”或查不到。
典型表现:dict[new Person("Bob", 25)] = "test"; 后再用另一个相同字段的 Person 去取,结果是 null —— 因为哈希桶找错了位置。
- 用字段计算哈希码时,只选参与
Equals判断的字段,且这些字段本身不能是可变的(比如public string Name { get; set; }就危险) - 推荐用
HashCode.Combine(field1, field2)(.NET Core 2.1+),比手写异或更安全,能更好处理null和顺序敏感问题 - 如果类是可变的,又想支持值相等,考虑标记为
readonly struct或用record(C# 9+)自动处理
record 类型让值相等“开箱即用”
如果你用的是 C# 9 或更高版本,且模型本质是“数据载体”,record 是最省心的选择。它自动生成基于所有属性的 Equals、GetHashCode、==、!=,甚至带非破坏性修改(with 表达式)。
对比:写一个 5 字段的 class,手动重写 Equals + GetHashCode 至少要 20 行,还容易漏判 null 或类型;而 record 一行搞定。
- 注意
record默认按所有声明的属性(包括init属性)做值比较,不需要额外标注 - 如果只想按部分字段比较(比如只比 ID),得显式重写
Equals,此时record的自动行为会被覆盖,但你仍得自己管GetHashCode -
record struct在 .NET 7+ 支持,适合高性能场景,但要注意它仍是值类型,this在方法中不可变(除非加ref)
== 运算符和 IEquatable<T> 是进阶优化点
== 默认也是引用比较。想让它支持值语义,得重载运算符;但光重载 == 不够,还得同时重载 !=,且内部逻辑必须和 Equals 一致,否则行为割裂。
而 IEquatable<T> 是为避免装箱和类型转换开销:当泛型集合(如 List<T>.Contains())调用 Equals 时,如果 T 实现了 IEquatable<T>,就会走泛型版本,跳过 object 参数的装箱和 as 转换。
- 实现
IEquatable<Person>后,必须让Equals(Person other)和Equals(object obj)行为完全一致,否则调用方无法预测结果 -
==运算符重载里,建议直接调用Equals(other),不要重复写字段比较逻辑,避免维护不一致 - 对引用类型,
IEquatable<T>主要提升泛型集合性能;对值类型,它还能避免不必要的装箱,值得加
最容易被忽略的一点:重写 Equals 后,如果这个类型会被序列化(比如 JSON.NET 或 System.Text.Json),值相等逻辑和序列化字段是否一致?比如你只在 Equals 里比了 ID,但序列化时把整个对象都存了,下游系统按全部字段判断相等,就会出现“代码里相等,存储后不等”的隐性不一致。










