C# record类型非破坏性突变时的属性条件赋值问题
场景与现状
我尝试将不可变的record类型用于部分DTO对象,发现非破坏性突变在简单属性赋值时写法简洁,但存在大量条件逻辑时会显得繁琐。
示例定义的Record:
public record MyDto { int? Id { get; init; } // 其他无关属性 }
业务逻辑:调用GET接口获取MyDto数据,本地修改后通过PUT请求回传。核心需求是仅当满足条件时为属性赋值,否则保持原值,比如仅在overrideId不为null时更新Id属性。
当前实现:
public MyDto UpdateTheDto(MyDto dto, int? overrideId) { return dto with { Id = overrideId ?? dto.Id }; }
疑问
- 是否有更优语法?例如,将
Id改为可变(用set替代init)是否更好? - 这会影响非破坏性突变的性能吗?官方文档提到非破坏性突变只会修改部分属性而非克隆整个record,但当
overrideId为null、最终使用dto.Id时,编译器能否识别这是无操作,表现得如同未赋值Id一样?
同时不确定不可变record是否适合当前使用场景。
问题解答
1. 语法优化与可变属性的取舍
更简洁的条件赋值写法
你现在用??的写法已经够简洁了,如果要处理多属性的条件更新,写个针对性的扩展方法会更清爽:
// 给MyDto写专属扩展 public static MyDto WithIdIf(this MyDto dto, int? overrideId) { return overrideId is null ? dto : dto with { Id = overrideId }; }
调用的时候直接return dto.WithIdIf(overrideId);,多属性的话还能链式调用,可读性拉满。要是有很多类似的属性要处理,这种方式比堆一堆??要干净得多。
改用set的利弊
改成set确实能让代码更直接:
public MyDto UpdateTheDto(MyDto dto, int? overrideId) { if (overrideId != null) dto.Id = overrideId; return dto; }
但代价是丢了不可变record的核心好处:不可变类型天生线程安全,而且不会出现“改了一个地方,其他引用这个DTO的地方也跟着变”的意外副作用。如果你的DTO会在多个方法、模块之间共享引用,这种隐性bug排查起来会很头疼。
2. 非破坏性突变的性能问题
先给你明确结论:编译器不会识别这种无意义的赋值。只要用了with表达式,不管你赋值的是不是原属性值,都会创建一个新的record实例,把原实例的所有属性值复制过来,再替换你指定的属性。
不过也不用慌,这种性能开销在DTO场景下基本可以忽略——record的复制是浅拷贝,值类型直接复制,引用类型只复制引用,速度非常快。真要在意的话,手动加个条件判断就行:
public MyDto UpdateTheDto(MyDto dto, int? overrideId) { return overrideId == null ? dto : dto with { Id = overrideId }; }
这样当overrideId是null时直接返回原实例,不会创建新对象。
不可变Record是否适合当前场景?
看你的业务需求来定:
- 如果你的DTO需要在多线程环境下用,或者要跨层传递、被多个地方引用,那不可变record绝对值得用——它能保证数据不会被偷偷修改,排查问题时能少掉好多头发。
- 如果你的DTO只是在单个方法里临时修改,不会被多个地方共享引用,那用带
set的record甚至普通class会更省事,省掉创建新实例的步骤。
大部分DTO场景下,我更推荐用不可变record,毕竟不可变性带来的稳定性,比那点微小的性能开销重要得多。
内容的提问来源于stack exchange,提问作者void.pointer

