record相等性是值比较而非引用比较,基于所有public属性值相同即相等,支持继承链类型对齐,但不支持跨类型或深比较,需谨慎处理浮点、null及可变引用类型。

Record相等性不是引用比较,而是值比较
默认情况下,record 类型重载了 Equals 和 GetHashCode,只要所有属性值相同,两个 record 实例就相等,哪怕它们是不同内存地址的对象。这和 class 的默认引用相等完全不同。
常见错误是误以为 record 和 class 在比较行为上一致,结果在字典键、集合去重、断言测试中出错——比如用 Assert.AreEqual 测试两个相同字段的 record 却失败,其实是没注意到构造方式或隐式转换影响了值。
- 使用
==或Equals()都会触发值比较(前提是没手动重写) - 如果 record 包含可变引用类型(如
List<int>),其内部内容变化不会影响 record 自身的相等性判断——因为比较的是引用本身,不是深比较 - 派生自
record的子 record 会自动参与相等性计算,父类字段也纳入比较范围
如何控制哪些字段参与相等性比较
record 的相等性基于 EqualityContract 和每个 init 或 get 属性的值。但你可以通过 with 表达式、自定义 Equals 或标记 [PrimaryConstructor] 外的字段为 private 来间接排除某些成员。
真正可控的方式只有一种:不把不想参与比较的字段声明为 record 的位置参数或自动属性。
- 位置参数(
record Person(string Name, int Age))全部参与比较 - 在 body 中额外声明的
public自动属性(public string Id { get; init; })也会被纳入比较 - 加
private修饰符的字段(private DateTime _createdAt;)不参与,除非你手动重写Equals - 想彻底绕过默认逻辑?重写
Equals(object? other)和GetHashCode(),但会失去编译器生成的语义一致性
与 class 混用时的 Equals 行为陷阱
当 record 和 class 实例调用 Equals,比如 myRecord.Equals(myClass),结果几乎总是 false——不仅因为类型不同,更因为 record 的 Equals 实现会先检查 other 是否为同类型或派生类型。
典型场景:DTO 转换后做断言,或泛型方法里统一用 Equals 判断,结果意外失败。
- 不要依赖跨类型
Equals的“合理”行为;显式转换或用EqualityComparer<T>.Default更安全 -
record的EqualityContract属性返回一个Type,用于运行时类型匹配;若other类型与此不兼容,直接短路返回false - 若必须混合比较,建议提取公共接口 + 自定义比较器,而不是靠默认
Equals
性能与调试时要注意的细节
编译器为 record 自动生成的 Equals 是逐字段比较,没有深拷贝开销,但字段越多越慢。调试时容易忽略的是:null 值比较、NaN 浮点数、以及自定义 IEquatable<T> 实现是否被正确调用。
- 浮点字段(
double/float)参与比较时,NaN == NaN返回false,record 也遵循此规则;必要时用double.IsNaN()预处理 - 如果 record 包含
object字段,其Equals调用仍走该对象自身的逻辑,可能引入不可控行为 - 反编译看生成代码会发现:相等性逻辑实际在
PrintMembers和EqualityContract协同下完成,调试时别只盯Equals方法体
最常被忽略的一点:record 的相等性契约在继承链中是“严格类型对齐”的,哪怕两个 record 结构完全一样,只要类型名不同,就不可能相等——这不是 bug,是设计使然。


















