record 是 C# 中具有不可变语义的引用类型,默认值相等、自动生成 ToString 和 with 表达式;而 class 默认引用相等、无自动成员生成,且可变。

record 是啥,跟 class 有啥实际区别
record 在 C# 里不是语法糖,是编译器生成不可变语义的类型。它默认启用值相等(Equals、GetHashCode 按字段内容比),自动生成 ToString,还带 with 表达式——这些都不是 class 默认有的。
关键差异在行为,不在写法:
-
record的构造函数参数自动成为init属性(只能初始化时赋值) -
record的==和!=默认走值比较,class走引用比较 -
record继承链中,Equals会递归检查所有基类字段;class不会
别以为加个 record 关键字就“自动不可变”——如果字段是 mutable list 或 class 类型,照样能改里面的内容。
怎么定义一个真正不可变的 record
真正不可变 = 外部不能改状态 + 内部没有可变引用。光用 record 不够,得配合类型选择和构造约束:
- 所有字段/属性必须是
init(不是set),且类型本身不可变 - 避免用
List<t></t>、Dictionary<k></k>这类可变集合,改用IReadOnlyList<t></t>或ImmutableArray<t></t> - 如果必须包含可变对象,构造时要深拷贝(比如用
new List<t>(source)</t>) - 构造函数里别暴露内部集合的引用(别直接
this.items = items;,要this.items = items.ToList().AsReadOnly();)
示例:
public record Person(string Name, int Age, IReadOnlyList<string> Tags); // ✅ 安全:Tags 是只读接口,且 record 自动把 Tags 声明为 init-only 属性
with 表达式为啥有时不按预期工作
with 表达式本质是调用编译器生成的 Clone + 属性赋值,但它只复制当前 record 的字段,不递归克隆嵌套的引用类型。
常见错误现象:
- 修改
with后对象的某个List<t></t>字段,原对象也跟着变 -
with之后再改嵌套对象状态,两个 record 共享同一份底层数据
原因:
-
with对class类型字段只是浅拷贝(复制引用) - 对
record字段才做深拷贝(调用其Clone)
所以别指望 with 让整个对象图不可变。如果字段是 class,得自己在 with 后手动重建嵌套结构,或者把嵌套类型也改成 record。
什么时候不该用 record
record 不是万能替代品,尤其在以下场景容易翻车:
- 需要序列化到 JSON 且字段名要小写/驼峰,但又不想加
[JsonPropertyName]——record的位置参数会强制按声明顺序映射,字段名无法隐藏或重命名 - 要实现
IDisposable或含非托管资源 ——record不支持析构函数和Dispose的标准模式 - 性能敏感路径,频繁创建大量 record 实例 —— 每次
with都新建对象,GC 压力比复用class实例高 - 需要运行时动态添加/修改属性(比如 ORM 映射、DTO 绑定)——
record的属性是编译期固定的,反射获取不到“可写性”以外的元数据
复杂点在于:不可变性是分层的。record 只管顶层字段的赋值控制,管不了你塞进去的那个 class 里藏着几个 public set。这点最容易被忽略。


















