with 表达式仅适用于 record 类型,因其依赖编译器生成的克隆逻辑、init 属性和值语义;普通 class/struct 不支持,会报 cs8852 错误;record struct 同样支持但为浅拷贝,嵌套引用不复制;需用不可变类型(如 immutablearray)或手动深拷贝确保真正不可变。

with 表达式不是语法糖,而是编译器强制保障“不可变语义”的关键机制——它只对 record 类型(含 record class 和 record struct)有效,普通 class 或 struct 直接用会报 CS8852 错误。
为什么 with 只能用于 record?
因为 with 依赖编译器自动生成的底层能力:克隆逻辑 + init 属性支持 + 值语义一致性。普通 class 没有这些,写 obj with { Prop = value } 就会编译失败。
- 漏写
record关键字是常见错误——public class Person { public string Name { get; init; } }不行,必须是public record Person(string Name)或显式声明public record Person { public string Name { get; init; } } - 继承自
record的子类,也必须自己加record关键字,否则不支持with -
record struct同样支持with,但要注意:每次with都产生新副本,原变量值不变(值类型语义)
with 是浅拷贝,嵌套引用字段不会被复制
如果 record 里包含 List<string></string>、Dictionary<int string></int> 或自定义类字段,with 只复制该字段的引用,不创建新集合或新对象。
- 后果:修改新对象里的集合,原对象也会看到变化 ——
r2.Hobbies.Add("B")会让r1.Hobbies也多出一项 - 修复方式:把可变引用类型换成不可变替代品,比如
IReadOnlyList<string></string>、ImmutableArray<t></t>,或字段本身也定义为record - 没有内置深拷贝支持;真要深拷贝,得手写逻辑或引入第三方库(如
System.Text.Json序列化再反序列化),with不负责也不提供配置项
在 init setter 里调用 with 很危险
看似合理,实则极易触发无限递归。编译器生成的 with 逻辑会走 init setter,而你在 setter 里又调用了 with,就形成闭环。
- 典型陷阱:
public string Name { get => _name; init { _name = value ?? throw new ArgumentNullException(); } }看似安全,但如果在init里写了this with { ... },就会崩 - 验证逻辑应放在构造函数或独立方法中,避免在
init中触发任何可能再次调用with的操作 -
with初始化器内部不能用this.Xxx访问当前实例成员,因为此时对象尚未完成构造
嵌套更新需要显式支持 init 访问器
with 本身不支持点号链式更新(如 p with { Address.Street = "Main" }),C# 不允许这种语法。必须分层处理:
- 先用
with更新嵌套对象本身:var updated = p with { Address = p.Address with { Street = "Main" } }; - 前提是
Address字段类型也是record,且其属性有init访问器 - 如果嵌套类型是普通
class,就得手动 new 一个新实例赋值,with帮不上忙
真正容易被忽略的点是:with 的安全性完全依赖于整个数据结构的“不可变纵深”。哪怕顶层是 record,只要其中任意一层用了可变引用类型(比如 public List<item> Items { get; init; }</item>),你就没获得真正的不可变性——这和语言设计无关,是建模时的选择问题。










