掘金 人工智能 08月14日
C#记录类型与集合的深度解析:从默认行为到自定义比较
index_new5.html
../../../zaker_core/zaker_tpl_static/wap/tpl_guoji1.html

 

本文作者分享了在使用C#记录类型和不可变集合构建选举网站时遇到的挑战。作者特别指出了记录类型在处理ImmutableList时的相等性比较问题,以及框架中缺乏直接的引用相等比较器。此外,文中还提到了字符串序数比较的注意事项以及Visual Studio中主构造函数和记录参数的调用层次结构显示问题。作者总结了功能需求,包括更灵活的相等比较器控制、不可变集合的比较器支持以及调用层次结构的改进,并对记录类型和不可变集合的整体表现给予了肯定,同时指出仍有改进空间。

🎯 **记录类型与相等性比较的挑战**:C#记录类型默认对每个属性进行相等性比较,这在处理ImmutableList时出现问题,因为ImmutableList本身不重写Equals和GetHashCode,仅支持引用相等。作者期望能够为特定属性指定自定义的相等比较器,以便正确比较ImmutableList的内容,而不是引用。

🔗 **引用相等比较器的需求与实现**:作者在构建视图模型时,需要对ImmutableDictionary中的Constituency进行引用比较,但在框架中未能直接找到实现IEqualityComparer的引用比较器。后经指出,System.Collections.Generic.ReferenceEqualityComparer虽然存在,但需要利用逆变特性进行隐式转换,这给开发者带来了一定的学习成本和使用不便。

🔠 **字符串序数比较的明确性**:作者对C#默认字符串比较中Equals/GetHashCode使用序数比较,而CompareTo使用文化敏感比较感到不安,倾向于所有字符串比较都采用序数比较。为此,作者创建了一系列扩展方法,如OrderByOrdinal、ToImmutableOrdinalDictionary等,以确保字符串比较的明确性和一致性。

💡 **Visual Studio中的开发体验痛点**:作者指出,在Visual Studio中,主构造函数和记录参数的“调用层次结构”功能存在问题,无法像其他属性一样方便地查看其调用关系,这影响了开发效率和代码理解。

🚀 **功能改进建议**:为了减少在记录类型和集合使用中的摩擦,作者提出了四点功能性改进建议:1. 控制生成代码中各属性的相等比较器;2. 为不可变集合提供可指定元素比较方式的相等比较器;3. 提供执行引用比较的IEqualityComparer实现;4. 改进主构造函数和记录构造函数调用的“调用层次结构”显示。

记录类型与集合

本文某种程度上是我在选举网站中使用记录类型和集合时遇到的各种摩擦点的汇总。

记录类型回顾

这可能是本系列中最具普适价值的博客文章。虽然记录类型自C# 10就已存在,但我个人使用不多(尽管我期待使用它们已有十余年,这是另一回事了)。

决定将所有数据模型设为不可变后,在C#中使用记录类型(我全部使用密封记录)来实现这些模型几乎是理所当然的选择。只需用主构造函数相同的格式指定所需属性,编译器就会自动生成大量样板代码。

以简单示例为例,考虑以下记录声明:

public sealed record Candidate(int Id, string Name, int? MySocietyId, int? ParliamentId);

这生成的代码大致等效于:

public sealed class Candidate : IEquatable<Candidate>{    // 属性声明、构造函数、Equals、GetHashCode等完整实现    // 包含解构方法和with表达式支持}

(此处保留了完整的类结构说明但压缩了具体实现细节)

记录类型的相等性比较

如上所示,记录类型默认使用EqualityComparer<T>.Default对每个属性进行比较。当属性类型的默认比较器符合需求时这很完美——但并非总是如此。在我们的选举数据模型中,大多数类型没问题,但ImmutableList<T>不符合要求,而我们大量使用了它。

ImmutableList<T>本身没有重写EqualsGetHashCode方法——因此具有引用相等语义。我真正需要的是使用元素类型的相等比较器,判断两个不可变列表在元素数量相同且元素成对相等时视为相等。这很容易实现——连同合适的GetHashCode方法。虽然可以包装成实现IEqualityComparer<ImmutableList<T>>的类型,但我目前尚未这样做。

遗憾的是,C#记录类型的工作方式无法为特定属性指定相等比较器。如果直接实现EqualsGetHashCode方法,这些自定义版本会替代生成版本,但意味着需要为所有属性实现比较逻辑。添加新属性时必须记得修改这两个方法(我至少忘记过一次)——而如果使用默认生成实现,添加新属性就非常简单。

引用相等比较

根据我关于数据模型的文章,在单个ElectionContext中,我们只需要引用相等。网站永远不需要通过从一个上下文指定另一个上下文的Constituency来获取2024年选举的选区结果。实际上,如果发现有这样的代码,很可能意味着存在bug:任何给定Web请求中的所有内容都应引用相同的ElectionContext

因此,当创建ImmutableDictionary<Constituency, Result>时,我希望提供仅执行引用比较的IEqualityComparer<Constituency>。虽然这看起来简单,但我发现当上下文重新加载时,这对构建视图模型的时间有显著影响。

我原以为在框架中很容易找到引用相等比较器——但如果真有,我错过了。

更新:感谢Michael Damatov指出,框架中确实存在System.Collections.Generic.ReferenceEqualityComparer——但它实现的是非泛型的IEqualityComparer<object>。我愚蠢地忽略了IEqualityComparer<T>的逆变特性:存在从IEqualityComparer<object>到任何类类型XIEqualityComparer<X>的隐式引用转换。

字符串序数比较

字符串比较让我紧张。虽然默认字符串比较对EqualsGetHashCode使用序数比较,但对CompareTo使用文化敏感比较。由于我几乎总是想要序数比较,因此喜欢明确指定。为此我创建了一系列扩展方法:

Visual Studio中的主构造函数和记录"调用层次结构"问题

在Visual Studio中,对主构造函数和记录参数,"查找引用"(Ctrl-K, Ctrl-R)有效但"调用层次结构"(Ctrl-K, Ctrl-T)无效。虽然可以理解主构造函数参数不支持,但记录参数最终成为属性,我期望能像其他属性一样查看调用层次结构。

更令人沮丧的是无法查看"调用构造函数"的层次结构。由于类/记录声明某种程度上也充当构造函数的声明,我原以为将光标放在类/记录声明上(在名称上)会起作用。

功能需求总结

总结来说,虽然我喜欢记录类型和不可变集合,但可以通过引入以下内容减少摩擦:

    控制生成代码中每个属性使用的相等比较器的方式不可变集合的相等比较器,能指定元素比较方式执行引用比较的IEqualityComparer<T>实现显示主构造函数和记录构造函数调用的"调用层次结构"

结论

我在记录类型和集合中发现的一些问题至少某种程度上特定于我的选举网站,尽管我强烈怀疑我不是唯一在记录中使用不可变集合并希望在相等比较中使用它们的开发者。

总体而言,记录类型在网站中表现良好,我很高兴它们可用,即使仍有改进空间。同样,能自然使用不可变集合也很棒——但在执行比较时提供更多帮助会更好。

Fish AI Reader

Fish AI Reader

AI辅助创作,多种专业模板,深度分析,高质量内容生成。从观点提取到深度思考,FishAI为您提供全方位的创作支持。新版本引入自定义参数,让您的创作更加个性化和精准。

FishAI

FishAI

鱼阅,AI 时代的下一个智能信息助手,助你摆脱信息焦虑

联系邮箱 441953276@qq.com

相关标签

C# 记录类型 不可变集合 相等性比较 编程实践
相关文章