record不是“写了就不可变”的魔法开关,它仅控制字段引用的赋值时机(init-only),不保证字段内部状态不可变;真正不可变需选用不可变类型、防御性拷贝及杜绝可变引用暴露。

record 不是“写了就不可变”的魔法开关,它只管字段引用的赋值控制,不管字段内部是否可变。真正不可变,得靠你选对类型、堵住所有修改入口。
record 的 init-only 属性到底锁住了什么
声明 public record Person(string Name, int Age) 后,Name 和 Age 自动变成 init 属性——这表示它们只能在构造时(包括 new Person(...) 或 p with { ... })赋值一次,之后任何直接赋值都会触发编译错误 CS8852。
- 它不阻止你在方法里写
this.Name = "xxx"(除非你删掉自动生成的initsetter,改用get-only) - 它不阻止你把一个可变对象(比如
List<string>)塞进去,然后在外面调用.Add() - 它不阻止你暴露
public List<string> Items { get; init; }这种属性——外部拿到引用后,照样能改内容
为什么 with 表达式改了 A 却影响了 B
因为 with 是浅拷贝:它只新建 record 实例,再把原实例字段的引用值复制过去,不会递归克隆嵌套对象。
- 如果你用
public record Order(string Id, List<Item> Items),那么var o2 = o1 with { Id = "new" }之后,o1.Items和o2.Items指向同一个List<Item> - 后续对
o1.Items.Add(...)的调用,o2.Items立刻可见 - 正确做法是换用
IReadOnlyList<Item>或ImmutableArray<Item>,并在构造时用.ToImmutableArray()转换
怎样定义一个真正不可变的 record
关键不是加 record,而是切断所有可变出口。每一步都得手动检查:
- 所有字段必须是
init或get-only;位置 record 的参数默认满足,但类体形式要显式写get; init; - 避免使用
List<T>、Dictionary<K,V>、T[]等可变集合;改用IReadOnlyList<T>、ImmutableArray<T>、ReadOnlyMemory<T> - 如果必须接收可变集合(比如从 API 解析),构造函数里要做防御性拷贝:
this.items = items.ToList().AsReadOnly(); - 不提供任何 public 方法去修改内部状态;哪怕 record 允许在方法里改
this.x,也别这么干——那违背了不可变契约
最常被忽略的一点:record 的不可变性是“表面层”的,你得自己为每一层嵌套数据负责。哪怕顶层是 record,只要里面套了个 class 或可变集合,整条链就失效了。


















