record 是带语义的不可变数据容器,启用值相等、init-only 属性、非空保障及自动生成 tostring/equals/gethashcode;误当普通 class 用易踩坑。

Record 类型不是语法糖,是带语义的不可变数据容器
Record 类型在 C# 9+ 中不是“更简洁的 class 写法”,它默认启用值相等、不可变属性、非空保障和自动生成 ToString/Equals/GetHashCode —— 这些行为是编译器强制注入的,不是靠约定或文档提醒。如果你把它当普通 class 用(比如手动加 setter、改字段、忽略 init 语义),很快会踩坑。
-
record的属性默认是init,不是set:赋值只能在对象初始化时(new Person("A")或with表达式),之后再改会编译报错 - 继承
record时,基类字段不会自动参与派生类的相等判断,除非显式写进构造参数列表 - 如果 record 包含可变引用类型(比如
List<string></string>),它仍是“逻辑可变”的——record只管自身字段是否被重新赋值,不递归冻结内容
什么时候该用 record?看这三条硬标准
别因为“新”就用,record 的适用场景非常具体:你正在建模一组只读、可比较、可复制的数据载体,并且不希望它们被意外修改。典型如 DTO、API 响应结构、配置快照、领域事件载荷。
- 需要结构相等(
a == b比较内容而非引用)→ 用record;只需要引用相等 → 用class - 需要安全的副本构造(
person with { Age = 30 })→ 用record;需要复用同一实例或延迟加载 → 不适合 - 字段全部是
init友好类型(string、int、其他record)→ 安全;含Dictionary<string object></string>且需随时 Add/Remove → 别硬套,加个class更直白
with 表达式不是深拷贝,只是字段级浅复制
with 看起来像“复制并修改”,但它只复制 record 自身声明的字段,对字段内部的可变对象不做任何处理。这是最常被误读的一点。
record Person(string Name, List<string> Hobbies);
var p1 = new Person("Alice", new List<string> { "reading" });
var p2 = p1 with { Name = "Bob" };
p2.Hobbies.Add("coding"); // ← 这会同时影响 p1.Hobbies!
</string></string>
-
with复制的是字段值:对引用类型,复制的是引用本身,不是对象副本 - 如果需要真正隔离嵌套可变状态,得手动 new 新容器(比如
with { Hobbies = new List<string>(p1.Hobbies) }</string>) - 编译器不会警告你嵌套可变性问题,运行时才暴露,建议在 record 中尽量用
IReadOnlyList<t></t>或ImmutableArray<t></t>
泛型 record 和 sealed 隐含约束容易被忽略
C# 编译器为每个 record 自动生成一个隐式的 sealed 基类(System.Record),但泛型 record 在反编译后会暴露真实基类链,这对序列化、反射或依赖注入可能造成意外。
- 泛型 record(如
record Result<t>(T Value)</t>)无法被非泛型代码直接约束为 “所有 record 都实现某接口”——它没有统一基类,只有共通模式 - JsonSerializer 默认能处理 record,但若用了
JsonConverter<t></t>并依赖typeof(T).IsClass判断,会漏掉 record(record类型的IsClass == true,但某些旧版反射逻辑会跳过) - 不要给 record 加
virtual方法或试图 override 编译器生成的ToString—— 它们是 sealed 的,编译直接报错:'Person.ToString()' cannot override inherited member because it is not marked virtual, abstract, or override
record 的设计意图很清晰:它不鼓励继承和运行时多态,而是强调数据契约的明确性。一旦开始绕着它搞动态行为,不如退回 class + 手动实现 IEquatable<t></t>。











