C#记录类型与'with'操作符的深入探讨

本文深入探讨了C#记录类型中'with'操作符的行为,分析了其潜在陷阱,并介绍了如何通过自定义Roslyn分析器来避免这些问题,同时分享了在Election2029项目中的实际应用经验。

记录类型与’with’操作符再探

在我的上一篇博客中,我描述了C#记录类型的一些行为,这些行为虽然完全符合文档说明,但却出乎我的意料。本文是上一篇的后续,因此如果您还没有阅读前一篇,建议先阅读——我不会重复相同的内容。

这是问题吗?

上一篇博客发布到了Hacker News上,评论意见不一。一些人认为这是设计意图,另一些人则认为这是C#语言团队的糟糕决定。不出所料,最有见地的评论来自Eric Lippert,他引用了Programming Language Design and Implementation Stack Exchange网站上的一个帖子。Eric一如既往地给出了详尽的回答。

我认为意见分歧源于对"with"操作符请求内容的不同理解。Eric写道:

“with"会复制记录,仅更改您指定要更改的属性值,因此不应惊讶于其他内容未改变。

这并不是我对"with"的理解——我从未期望它表示"这个对象但具有这些不同的属性”,而是期望它表示"一个新记录,具有与原始记录相同的参数,但使用这些不同的参数"。这是一个微妙的区别——微妙到我直到遇到这个问题才去思考它——但我怀疑这解释了为什么不同的人对同一功能有不同的理解。

陷阱避免:Roslyn分析器

在上篇文章中,我提到编写Roslyn分析器作为一种可能的解决方案。我的初步希望是有一个单一的分析器,可以检测任何在初始化期间使用的参数的"with"操作符使用情况。

这一初步尝试在一定程度上是有效的——它可以检测到原始博客文章中的危险代码——但它仅在记录源代码和使用"with"操作符的源代码位于同一项目中时才有效。我现在有了一个更好的解决方案,包含两个分析器,甚至可以与包引用一起工作,即使您可能无法访问记录的源代码……只要包作者使用相同的分析器!

分析器的源代码在GitHub上,分析器本身在JonSkeet.RoslynAnalyzers NuGet包中。要在项目中安装它们,只需在项目文件的项组中添加以下内容:

1
2
3
<PackageReference Include="JonSkeet.RoslynAnalyzers" Version="1.0.0-beta.6"
        PrivateAssets="all"
        IncludeAssets="runtime; build; native; contentfiles; analyzers"/>

显然,这还处于测试阶段——目前可能还有许多角落情况无法检测到。(欢迎提交拉取请求。)但它暂时满足了我的特定需求。

Election2029的新模式

那么,当我在Election2029项目中启用分析器时发生了什么?(暂且忽略第一次不工作的部分……版本号已经是1.0.0-beta.6是有原因的。)

不出所料,一堆记录因未指定[DangerousWithTarget]而被标记……当我应用该属性后,只有一两个地方我以不安全的方式使用了"with"操作符。(当然,我检查了最初让我发现这个问题的错误是否被分析器捕获——确实捕获了。)

对于大多数记录,预计算对我来说感觉还可以。它们基本上仍然是轻量级记录,带有一些预计算,如果改为按需计算会感觉效率低下。我喜欢通过它们是记录这一事实自动获得的功能。我选择保留这些记录,知道至少如果我尝试以危险的方式使用"with"操作符,我会收到警告。

然而,有两种类型——ElectionCoreContext和ElectionContext——它们有很多预计算。它们更像是合理的类。最初,我将它们转换为"普通"类,带有主构造函数和属性。感觉还可以,但不知何故不太对劲。我喜欢记录类型仅用于上下文的规范信息的想法……所以我将ElectionContext转换成了这样(ElectionCoreContext也有类似的东西):

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
public sealed class ElectionContext : IEquatable<ElectionContext>
{
    public ElectionContextSourceData SourceData { get; }

    // 一堆代理访问的属性
    public Instant Timestamp => SourceData.Timestamp;
    // ...

    public ElectionContext(ElectionContextSourceData sourceData)
    {
        // 初始化和验证
    }

    public sealed record ElectionContextSourceData(Instance Timestamp, ...)
    {
        // Equals和GetHashCode,但没有其他内容
    }
}

结论

我很高兴现在可以更安全地使用记录,即使我以其他人可能不完全认可的方式使用它们。我可能会改变主意,回到对所有非最明确的情况使用常规类。但目前,“在感觉合适的地方使用记录,即使这意味着预计算"和"在有足够行为证明合理的地方使用类包装记录"的方法效果相当好。

我并不真的期望其他开发人员使用我的分析器(当然非常欢迎您这样做)——但它们甚至是可行的这一事实表明Roslyn有点奇迹。我还不推荐我的"谨慎使用记录稍微超出其预期用途"或"类包装记录"的方法。如果它们在Election2029项目中不起作用,我还有足够的时间进行重构。但我仍然有兴趣获得反馈,看看我的决定在其他人看来是否至少有些合理。

comments powered by Disqus
使用 Hugo 构建
主题 StackJimmy 设计