<p>Record 是 C# 10+ 中语法糖驱动的引用类型,能保证字段引用不可重赋值(编译期强制 init-only),但不保证嵌套对象或内部状态不变;真正不变需配合只读集合、Immutable 类型及杜绝可变字段。</p>

Record 是什么,它真能保证不变性?
Record 在 C# 10+ 中是语法糖驱动的引用类型,编译器自动生成 Equals、GetHashCode、ToString 和只读属性(init setter),但「不变性」不是魔法——它依赖你不暴露可变状态。Record 的字段默认是 init,意味着只能在对象构造时赋值,之后再赋值会编译报错:CS8852(Init-only property or indexer … can only be assigned in an object initializer)。这是编译期强制的不可变入口。
- Record 类型本身不阻止你在内部定义可变字段(比如
public int Counter;),这种字段仍可随时修改 - 如果 record 包含可变引用类型(如
List<string></string>),record 实例虽不可替换其字段引用,但该列表内容仍可被修改 - 使用
with表达式创建副本时,只是浅拷贝;原对象与新对象共享嵌套的可变引用
如何正确定义一个真正不变的 Record
要让 record 行为接近「逻辑不可变」,必须从结构上切断所有可变出口:
- 所有字段/属性必须声明为
init或get-only(推荐用init,支持位置语法) - 避免公开可变引用类型;改用
IReadOnlyList<t></t>、ImmutableArray<t></t>或ReadOnlyMemory<t></t>等只读接口 - 若需集合,优先用
System.Collections.Immutable中的类型,例如ImmutableArray<string></string>,并在构造时用.ToImmutableArray()转换 - 不提供 public setter、不暴露 backing field、不在方法中修改自身字段(record 方法里改
this.x = ...是允许的,但违背不变性原则,应避免)
示例:
public record Person(string Name, ImmutableArray<string> Tags)
{
// 正确:Tags 是不可变集合,Name 是 init-only
// 错误写法:public List<string> Tags { get; init; } —— 外部可调用 .Add()
}with 表达式为什么有时“失效”?
with 创建的是浅拷贝,它只复制 record 的直接字段,对字段中引用的对象不做深拷贝。如果你写:
var p1 = new Person("Alice", ImmutableArray.Create("a", "b"));
var p2 = p1 with { Name = "Bob" }; // ✅ 新实例,Name 改了,Tags 引用相同这没问题;但若你误用了可变类型:
public record BadPerson(string Name, List<string> Tags); // ❌ 危险!
var p1 = new BadPerson("Alice", new List<string> { "x" });
var p2 = p1 with { Name = "Bob" };
p1.Tags.Add("y"); // ✅ 合法 —— p2.Tags 也会看到 "y"
这就是典型的「看似不变,实则共享可变状态」。根本问题不在 with,而在字段类型选错了。
Record 和 struct、class 的关键区别在哪?
-
struct 是值类型,拷贝开销大、不能继承、默认无参数构造函数受限;record 是引用类型,语义更轻量,天然支持继承(record sealed 可禁用)
-
class 默认没有值语义(== 比较引用),而 record 默认重载 Equals 和 == 做结构相等比较
- record 的
with 是语法糖,底层调用的是 Clone() + 属性赋值;class 没这个机制,得手写复制逻辑
- record 不能有无参构造函数(除非显式定义),也不能在构造后调用
init 属性 setter——这点比 class 更严格,是编译器级防护
struct 是值类型,拷贝开销大、不能继承、默认无参数构造函数受限;record 是引用类型,语义更轻量,天然支持继承(record sealed 可禁用) class 默认没有值语义(== 比较引用),而 record 默认重载 Equals 和 == 做结构相等比较 with 是语法糖,底层调用的是 Clone() + 属性赋值;class 没这个机制,得手写复制逻辑 init 属性 setter——这点比 class 更严格,是编译器级防护 真正容易被忽略的是:record 的「不变性」完全建立在开发者对字段类型的审慎选择之上。编译器只管住字段引用是否可重赋值,不管住引用指向的对象内部是否安静。










