with表达式仅支持record类型(含record class/struct),不支持普通class或struct;其为浅拷贝,不递归复制引用对象;init setter中调用with易致无限递归,需避免。

with 表达式只能用于 record 类型
不是所有类都能用 with,只有 record(包括 record class 和 record struct)才支持。普通 class 或 struct 写 obj with { Prop = value } 会直接编译报错:CS8852: Init-only property or indexer 'X' can only be assigned in an object initializer, or on 'this' in an instance constructor。
常见错误是把老代码里的类改成 record 时漏掉 record 关键字,或者误以为继承自 record 的子类自动获得 with 能力——其实不行,子类也得显式声明为 record。
-
record是语法糖,底层靠生成Clone()+init属性 + 编译器合成的With方法实现 - 如果类型里有非
init的可变属性(比如只有set没有init),with表达式不会修改它,也不会报错,容易误以为改成功了 -
record struct同样支持with,但要注意值类型的语义:每次with都是新副本,原变量不变
with 会深拷贝还是浅拷贝?
with 表达式只做浅拷贝 —— 它复制字段值,不递归复制引用对象。如果 record 里有个 List<string></string> 字段,with 出来的新实例和原实例共享同一个 List 对象。
这在多线程或后续修改集合时容易出问题。比如:
var r1 = new Person("Alice", new List<string> { "A" });
var r2 = r1 with { Name = "Bob" };
r2.Hobbies.Add("B"); // r1.Hobbies 也会变成 ["A", "B"]- 想真正“不可变”,嵌套的引用类型字段本身也得是
record或不可变类型(如IReadOnlyList<t></t>、ImmutableArray<t></t>) - 没有银弹:如果必须深拷贝,得自己写逻辑,
with不负责、也不支持指定深度 -
with对 null 字段照常赋值,不会跳过或报空引用异常
with 表达式中访问 this 成员要小心循环引用
在 with 初始化器里,不能直接用 this.Xxx 引用当前实例成员,因为此时 this 尚未完成构造。但更隐蔽的问题是:如果你在 record 的 init 属性 setter 里又调用了 with,可能触发无限递归。
典型场景是带验证逻辑的 init setter:
public record Person(string Name)
{
public string Name { get; init; } = Name switch
{
null => throw new ArgumentNullException(nameof(Name)),
_ => Name
};
<pre class='brush:php;toolbar:false;'>// ❌ 危险:下面这个 WithName 方法如果内部用 with,就可能绕回这里
public Person WithName(string name) => this with { Name = name };}
- 编译器不会阻止你在
initsetter 里调用with,但运行时可能栈溢出 - 推荐做法:验证逻辑放在构造函数或工厂方法里,
initsetter 只做简单赋值 - 如果真需要基于当前值计算新值(比如自动补前缀),用普通方法返回新
record,别在init里搞复杂逻辑
record 继承链中 with 的行为
派生 record 调用 with 时,会同时初始化基类和派生类的所有 init 属性。但注意:基类字段名和派生类字段名冲突时,C# 9+ 要求显式限定,否则编译失败。
例如:
public record Animal(string Name); public record Dog(string Name, string Breed) : Animal(Name);
这时 new Dog("Lassie", "Collie") with { Name = "Rex" } 是合法的;但如果基类和派生类都有同名 Name,就得写成 with { Animal.Name = "Rex" }(需 C# 10+ 支持)。
- C# 9 不支持跨层级字段限定,遇到同名字段只能重命名基类属性
- 如果派生
record没有显式调用基类构造(比如漏了: base(...)),编译器会报错,with也无法工作 - 抽象
record不能实例化,自然也不能用with;但具体子类可以
实际用的时候,最常被忽略的是浅拷贝语义和 init setter 里的隐式递归风险。这两个点不写测试很难暴露,一上线就容易在并发或深层修改时翻车。


















