仅限 record(含 record class 和 record struct)支持 with 表达式,它是编译器硬性支持的浅拷贝机制,复制值类型字段和引用类型地址,不递归深拷贝,且 init setter 中调用 with 易致栈溢出。

with 表达式只对 record 类型有效,普通 class 或 struct 直接写 obj with { Prop = value } 会编译报错 CS8852。
哪些类型能用 with?
仅限 record(含 record class 和 record struct)。它不是语法糖,而是编译器为 record 自动生成克隆逻辑的硬性支持。
-
class Person { public string Name { get; init; } }—— ❌ 不行,CS8852 -
record Person(string Name);—— ✅ 行,with自动可用 -
record struct Point(int X, int Y);—— ✅ 行,同样支持
注意:record 的基类或继承链中混入普通 class 不影响 with 本身,但若字段是可变引用类型(如 List<int></int>),浅拷贝行为仍存在风险。
with 是浅拷贝,不是深拷贝
它复制字段值:值类型直接拷贝;引用类型只拷贝引用地址,不递归复制所指对象。这意味着原始对象和新对象共享嵌套的可变引用。
- 如果
record中有public List<string> Tags { get; init; }</string>,newObj = oldObj with { }后,newObj.Tags和oldObj.Tags指向同一个List - 后续调用
newObj.Tags.Add("x"),oldObj.Tags也会看到这个新增项 - 想规避?把字段换成不可变类型,比如
IReadOnlyList<string></string>或ImmutableArray<string></string>
别指望 with 自动帮你“冻结”嵌套结构 —— 它只管顶层字段的复制动作。
在 init setter 里调用 with 很危险
如果你在 record 的 init 属性 setter 中又用了 with,极易触发无限递归。编译器不会拦,运行时直接栈溢出。
public record Person(string Name)
{
public string Name { get; init; } = Name switch
{
null => throw new ArgumentNullException(nameof(Name)),
_ => Name
};
// ❌ 危险示例:WithName 方法若内部用 this with { ... },就可能绕回这里
public Person WithName(string name) => this with { Name = name };
}
- 上面的
WithName看似合理,但若Name的initsetter 再次调用WithName(比如做校验重试),就会死循环 - 安全做法:避免在
initsetter 中调用任何可能间接触发with的方法 - 更稳妥的替代:用构造函数显式创建新实例,绕过
with链路
with 赋值对 null 字段照常生效
哪怕某个字段当前是 null,with 依然会把它设成新值,不会跳过、也不会抛空引用异常。这看起来正常,但容易掩盖初始化疏漏。
- 例如
record User(string Name, Address? Addr);,var u1 = new User("A", null);,再写var u2 = u1 with { Addr = new Address(...) };—— 没问题 - 但如果业务逻辑本该要求
Addr永远非空,而你依赖with去补,就等于把校验责任推给了调用方 - 真正关键的约束,应放在构造函数或
initsetter 中,而不是靠with的赋值时机来兜底
最易被忽略的一点:浅拷贝 + 可变引用字段 + 多线程读写 = 隐形竞态。别只盯着 with 语法是否写对,得看整个数据图谱是否真的“不可变”。











