C# with表达式和记录类型 C#如何非破坏性地修改不可变对象

来源:这里教程网 时间:2026-02-21 17:40:18 作者:

with 表达式只能用于 record 类型或支持
Clone
+
with
模式的自定义类型

不是所有 C# 类都能用

with
。只有
record
(包括
record class
record struct
)原生支持
with
表达式;普通
class
struct
即使字段全只读,也不行。C# 编译器会为
record
自动生成一个隐藏的
Clone
方法和带初始化器的构造逻辑,
with
本质是调用这个机制。

常见错误现象:

CS8955 “with” expression cannot be applied to expression of type 'MyClass'
—— 这说明你试图对非 record 类型使用
with
,编译器直接拒绝。

若你已有旧类,想获得
with
能力,最简单方式是把它改写为
record class MyRecord(...)
record struct
同样支持
with
,但要注意值语义下复制开销,尤其含大数组或嵌套对象时
自定义类型可通过实现
Clone()
并重载
with
相关操作符模拟行为,但这是手动模拟,不被语言级
with
识别

record 的
with
是浅拷贝,嵌套 record 需显式更新

with
不会递归克隆嵌套的不可变对象。如果 record 字段本身是另一个 record,修改外层字段时,内层对象引用不变 —— 这是“非破坏性”的一部分,但也容易误以为已更新深层结构。

record Address(string Street, string City);
record Person(string Name, Address Addr);
<p>var p1 = new Person("Alice", new Address("123 St", "NYC"));
var p2 = p1 with { Name = "Bob" }; // OK:Addr 引用未变
var p3 = p1 with { Addr = p1.Addr with { City = "LA" } }; // 必须显式链式 with
漏掉嵌套
with
是最常见 bug 来源:你以为
p1 with { Addr.City = "LA" }
合法,但它语法错误 ——
with
只支持顶层字段赋值
字段是
string
int
等值类型或
record
时安全;若是
class
(如
List<t></t>
),即使外层是 record,内部集合仍可被意外修改
若需深不可变性,应避免在 record 中持有可变引用类型,或封装为只读包装(如
IReadOnlyList<t></t>

record 的
init
属性与
with
的配合关系

with
修改的字段必须声明为
init
(或
get; init;
),不能是纯
get;
。C# 编译器生成的
with
构造逻辑依赖
init
语义:允许在对象创建后一次性设置,之后冻结。

定义 record 时不写访问修饰符,默认所有位置参数都生成
init
属性;但若手动声明属性,必须显式写
public string Name { get; init; }
如果字段是
get; private set;
with
无法修改它 —— 编译器不认为它是“可 with 的”
混合使用:record 中可同时存在
init
字段(支持
with
)和只读字段(如
DateTime CreatedAt { get; } = DateTime.Now;
),后者在
with
中保持原值

性能与分配:每次
with
都创建新实例,没有就地修改

with
表达式必然分配新对象,无论是否实际修改字段。它不复用原实例内存,也不触发任何“变更检测”优化 —— 就是调用生成的克隆构造器,然后按需覆盖字段。

高频调用
with
(如游戏帧循环中更新 entity 状态)可能引发 GC 压力,此时应评估是否真需要不可变语义,或改用可变状态+手动快照
对比
struct
:record struct 的
with
是栈上复制,无 GC 开销,但值语义下传参/返回成本更高,且不适用于大尺寸数据
调试时注意:两个逻辑等价的 record 实例(字段值全同)用
==
比较返回
true
,但引用
ReferenceEquals
一定为
false

真正难的是设计好嵌套层级和边界 —— 什么时候该用 record,什么时候该用 sealed class + 手动 builder,取决于你是否需要结构相等、模式匹配,以及谁来控制“不可变”的粒度。

相关推荐