C#记录类型与集合的深度探讨:从自动生成代码到自定义相等性比较

本文深入探讨C#记录类型与不可变集合在实际应用中的痛点,包括自动生成代码机制、自定义属性级相等比较器需求、引用相等性优化,以及Visual Studio工具链支持不足等问题,并提出语言改进建议。

记录类型与集合

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

记录类型回顾

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

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

简单示例如下:

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

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

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

(为简洁起见省略了完整代码,实际还包含EqualityContract等细节)

记录类型的相等性比较

默认情况下,记录类型为每个属性使用EqualityComparer<T>.Default。当属性类型的默认相等比较器符合需求时这很完美——但并非总是如此。在我们的选举数据模型中,大多数类型没问题,但ImmutableList<T>不适用,而我们大量使用了这种类型。

ImmutableList<T>本身没有重写EqualsGetHashCode——因此具有引用相等语义。我真正需要的是使用元素类型的相等比较器,判断两个不可变列表是否具有相同数量的元素,且元素按顺序成对相等。

遗憾的是,C#记录类型目前无法为单个属性指定自定义相等比较器。如果手动实现EqualsGetHashCode方法,就需要为所有属性实现比较逻辑——这意味着添加新属性时必须记得更新这些方法(我就至少忘记过一次)。

我期望的解决方案是能够通过特性指示编译器使用指定的相等比较器提供程序。例如:

1
2
3
4
public sealed record Ballot(
    Constituency Constituency,
    [IEqualityComparerProvider(typeof(CollectionEqualityProvider))] 
    ImmutableList<Candidacy> Candidacies);

引用相等性

在我的数据模型文章中提过,在单个ElectionContext中,我们只需要引用相等性。创建ImmutableDictionary<Constituency, Result>时,我希望提供仅执行引用比较的IEqualityComparer<Constituency>。虽然这看似简单,但发现它显著影响了上下文重载时构建视图模型的时间。

.NET 5+实际上已经提供了System.Collections.Generic.ReferenceEqualityComparer,但我最初错误地忽略了它——因为它实现了非泛型的IEqualityComparer<object>。后来才意识到IEqualityComparer<T>是逆变的,完全可以安全使用。

字符串序数比较

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

  • OrderByOrdinal
  • OrderByOrdinalDescending
  • ToImmutableOrdinalDictionary(4个重载)
  • ToOrdinalLookup(2个重载)

Visual Studio工具链问题

在VS中使用"调用层次结构"(Ctrl+K, Ctrl+T)时:

  1. 主构造函数和记录参数的"调用层次结构"不工作(尽管"查找引用"可以)
  2. 无法查看"调用构造函数"的层次结构

功能需求总结

  1. 支持按属性控制生成的相等比较逻辑
  2. 为不可变集合提供内置相等比较器
  3. 提供泛型引用相等比较器实现
  4. 增强VS对记录类型的工具支持

结论

尽管存在这些摩擦点,记录类型在站点中表现良好,不可变集合的天然支持也很棒——只是在比较操作方面还需要更多支持。

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