我最初接手一个 C# 9 项目时,看到团队里几乎所有 DTO 都被写成了 record,理由是“代码简洁、值相等性开箱即用”。但到了代码评审阶段,总有同事追问:record 底层到底比 class 多做了什么?高并发下频繁 new、Equals、with 会不会成为瓶颈?当时我给不出让人信服的数据,只能回一句“应该差别不大”。后来为了对线上一个热点路径做优化,我专门把 record 和 class 从 IL 层面、堆栈分配、相等判断、典型基准几个角度完整对比了一遍,才彻底弄明白两者的性能差异到底来自哪里。这篇文章就把我踩过的坑和实测结论梳理出来,适合正在做 .NET 技术选型、或者想把老代码中的 class 改成 record 但又担心性能的同学参考。
1. record 不是“加了语法糖的 class”,它与普通 class 的差异在编译前就已经注定了
先说个容易混淆的点:很多人把 C# 9 引入的 record 等价理解成“编译器帮你生成了 equals/hashcode 的 class”,这个说法方向对,但不完整。C# 里其实有两类 record:record class 和 record struct。后者的底层是值类型,和 struct 一样可以分配在栈上,也可以作为泛型参数内联到集合中。所以如果你把 record class 和 class 做性能对比,本质上是在比较参考类型和参考类型;而 record struct 参与对比时,才会牵涉到“值复制”和“栈分配”这些完全不同的开销模型。
差异在定义层面就已经出现:
csharp复制// 普通类
public class PersonClass
{
public string Name { get; set; }
public int Age { get; set; }
}
// record 类
public record PersonRecord(string Name, int Age);
// record 结构体
public record struct PersonStruct(string Name, int Age);
同样的字段,PersonRecord 编译后比 PersonClass 多了一堆合成成员:EqualityContract、PrintMembers、重写的 ToString、Equals、GetHashCode、Clone、以及支持 with 表达式的内部复制方法。如果写成位置参数形式,编译器还会生成一个 Deconstruct 析构方法。这些都意味着更多 IL 指令和更多的运行时调用路径。
普通 class 里的 Equals 和 GetHashCode 来自 object 基类,默认走的是引用相等和运行时身份哈希;record 则完全重写掉了这套逻辑,改成逐字段比较。所以严格说,record 不是“同样的 class 加上语法糖”,而更像“编译器替你实现了一个自定义值语义模型”的引用类型。这里有一个很关键的性能前提:任何自定义相等逻辑,都比 object 默认的引用比较要昂贵。当然,如果你本来就在 class 里手写了 Equals 和 GetHashCode,那 record 带来的相对开销反而没那么大。
我建议在纠结性能前,先把语义问题列清楚。record 默认是“只要字段相同,两个对象就相等”,class 默认是“只有同一个引用才相等”。前者适合用来表示值对象、消息体、配置项;后者适合表示有唯一身份的领域实体。性能优化永远建立在语义正确的基础上,否则你为了快 10 纳秒用了错误的相等模型,代码里埋下的坑远比收益大。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 从 IL 看两者本质差异:编译器在 record 里加装了好几张“预制表”
我实际用 ILSpy 看过编译后的程序集。PersonClass 的 IL 很简单,只有构造函数、属性 getter/setter;但 PersonRecord 的 IL 明显膨胀,主要包含这些成员:
| 成员 | record class | 普通 class | 说明 |
|---|---|---|---|
| 构造函数 | 生成一个主构造函数 | 自行定义 | 位置参数 record 会把主构造参数赋值给属性 |
EqualityContract |
生成,protected 属性 | 无 | 用于多态相等判断:要求类型完全一致 |
Equals(object) |
重写 | 默认引用比较 | record 会调用 IEquatable<T> |
Equals(T) |
生成 | 无 | 诸字段用 `EqualityComparer |
