写点什么

C#记录类型与集合的深度解析:从默认行为到自定义比较

作者:qife122
  • 2025-08-14
    福建
  • 本文字数:1876 字

    阅读完需:约 6 分钟

记录类型与集合

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

记录类型回顾

这可能是本系列中最具普适价值的博客文章。虽然记录类型自 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使用文化敏感比较。由于我几乎总是想要序数比较,因此喜欢明确指定。为此我创建了一系列扩展方法:


  • OrderByOrdinal

  • OrderByOrdinalDescending

  • ToImmutableOrdinalDictionary(4 个重载)

  • ToOrdinalDictionary(4 个重载)

  • ToOrdinalLookup(2 个重载)

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

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


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

功能需求总结

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


  1. 控制生成代码中每个属性使用的相等比较器的方式

  2. 不可变集合的相等比较器,能指定元素比较方式

  3. 执行引用比较的IEqualityComparer<T>实现

  4. 显示主构造函数和记录构造函数调用的"调用层次结构"

结论

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


总体而言,记录类型在网站中表现良好,我很高兴它们可用,即使仍有改进空间。同样,能自然使用不可变集合也很棒——但在执行比较时提供更多帮助会更好。更多精彩内容 请关注我的个人公众号 公众号(办公 AI 智能小助手)公众号二维码


办公AI智能小助手


用户头像

qife122

关注

还未添加个人签名 2021-05-19 加入

还未添加个人简介

评论

发布
暂无评论
C#记录类型与集合的深度解析:从默认行为到自定义比较_C#_qife122_InfoQ写作社区