C#记录类型中意外的数据不一致问题
最近我在调试代码时发现了一个bug,结果是我对C#记录类型工作方式的误解。可能只有我一个人期望它们以这种方式工作,但我觉得值得写出来分享。
回顾:非破坏性突变
当记录类型引入C#时,同时引入了带有with
操作符的"非破坏性突变"概念。记录类型可以是不可变的,但你可以轻松高效地创建新实例,这些实例具有现有实例的数据,但某些属性值不同。
例如,假设你有这样的记录:
|
|
你可以编写如下代码:
|
|
这不会改变第一个实例中的数据(所以entry.Score
仍然是5000)。
回顾派生数据
记录类型不允许为主要构造函数指定构造函数体,但你可以基于主要构造函数中的参数值初始化字段(以及自动实现的属性)。
作为一个简单的示例,你可以创建一个记录,在初始化时确定值是否为奇数或偶数:
|
|
乍一看这很好:
|
|
到目前为止一切顺利。直到本周,我还以为这一切都没问题。
问题:混合使用with和派生数据
问题出现在混合使用这两个功能时。如果我们更改上面的代码(保持记录本身不变),使用with
操作符而不是调用构造函数来创建第二个Number,输出就变得不正确:
|
|
“Value = 3, Even = True"真的很不好。
这是怎么发生的?出于某种原因,我一直假设with
操作符使用新值调用构造函数。实际上并非如此。上面的with
操作符大致翻译成这样的代码:
|
|
<Clone>$
方法(至少在这种情况下)调用生成的复制构造函数(Number(Number)),它复制Value和Even的支持字段。
这一切都有文档记录——但目前没有任何关于可能引入的不一致性的警告。
我们可以做什么?
到目前为止,我想到了四种前进的方法,都不令人愉快。
选项1:耸肩继续生活
现在我知道了这个问题,我可以避免在除了"简单"记录之外的任何东西上使用with
操作符。如果没有计算属性或字段,with
操作符仍然非常有用。
选项2:编写Roslyn分析器来检测问题
理论上,至少对于在声明它们的同一解决方案中使用的任何记录,编写Roslyn分析器应该是可行的,该分析器可以:
- 分析每个声明记录中的每个成员初始化器,查看使用了哪些参数
- 分析每个
with
操作符用法,查看正在设置哪些参数 - 如果两者之间存在任何交集,则记录错误
选项3:找出安全使用with的方法
我一直在尝试如何使用Lazy<T>
来延迟计算任何属性,直到它们第一次被使用,这将在with
操作符为属性设置新值之后发生。
选项4:请求更改语言
我提出这一点只是为了完整性。我对C#设计团队非常信任:他们是聪明的人,会仔细思考问题。
结论
在C#中发现陷阱是非常罕见的,但这对我来说确实像一个。也许这只是因为我在选举网站中如此广泛地使用了计算属性——也许记录真的不是设计用来这样使用的,我的一半记录类型真的应该是类。
我不想停止使用记录,也绝对不鼓励其他人这样做。我不想停止使用with
操作符,再次强调,我也不鼓励其他人这样做。我希望这篇文章能对那些以不安全的方式使用with
的人起到一点警示作用。