记录类型与集合
本文某种程度上是我在选举网站中使用记录类型和集合时遇到的各种摩擦点的汇总。
记录类型回顾
这可能是本系列中最具普适价值的博客文章。虽然记录类型自C# 10就已存在,但我个人使用不多(尽管我期待使用它们已有十余年,这是另一回事了)。
决定将所有数据模型设为不可变后,在C#中使用记录类型(我全部使用密封记录)来实现这些模型几乎是理所当然的选择。只需用主构造函数相同的格式指定所需属性,编译器就会自动生成大量样板代码。
以简单示例为例,考虑以下记录声明:
csharp
public sealed record Candidate(int Id, string Name, int? MySocietyId, int? ParliamentId);
这生成的代码大致等效于:
csharp
public sealed class Candidate : IEquatable<Candidate>
{
// 属性声明、构造函数、Equals、GetHashCode等完整实现
// 包含解构方法和with表达式支持
}
(此处保留了完整的类结构说明但压缩了具体实现细节)
记录类型的相等性比较
如上所示,记录类型默认使用EqualityComparer<T>.Default
对每个属性进行比较。当属性类型的默认比较器符合需求时这很完美------但并非总是如此。在我们的选举数据模型中,大多数类型没问题,但ImmutableList<T>
不符合要求,而我们大量使用了它。
ImmutableList<T>
本身没有重写Equals
和GetHashCode
方法------因此具有引用相等语义。我真正需要的是使用元素类型的相等比较器,判断两个不可变列表在元素数量相同且元素成对相等时视为相等。这很容易实现------连同合适的GetHashCode
方法。虽然可以包装成实现IEqualityComparer<ImmutableList<T>>
的类型,但我目前尚未这样做。
遗憾的是,C#记录类型的工作方式无法为特定属性指定相等比较器。如果直接实现Equals
和GetHashCode
方法,这些自定义版本会替代生成版本,但意味着需要为所有属性实现比较逻辑。添加新属性时必须记得修改这两个方法(我至少忘记过一次)------而如果使用默认生成实现,添加新属性就非常简单。
引用相等比较
根据我关于数据模型的文章,在单个ElectionContext
中,我们只需要引用相等。网站永远不需要通过从一个上下文指定另一个上下文的Constituency
来获取2024年选举的选区结果。实际上,如果发现有这样的代码,很可能意味着存在bug:任何给定Web请求中的所有内容都应引用相同的ElectionContext
。
因此,当创建ImmutableDictionary<Constituency, Result>
时,我希望提供仅执行引用比较的IEqualityComparer<Constituency>
。虽然这看起来简单,但我发现当上下文重新加载时,这对构建视图模型的时间有显著影响。
我原以为在框架中很容易找到引用相等比较器------但如果真有,我错过了。
更新 :感谢Michael Damatov指出,框架中确实存在System.Collections.Generic.ReferenceEqualityComparer
------但它实现的是非泛型的IEqualityComparer<object>
。我愚蠢地忽略了IEqualityComparer<T>
的逆变特性:存在从IEqualityComparer<object>
到任何类类型X
的IEqualityComparer<X>
的隐式引用转换。
字符串序数比较
字符串比较让我紧张。虽然默认字符串比较对Equals
和GetHashCode
使用序数比较,但对CompareTo
使用文化敏感比较。由于我几乎总是想要序数比较,因此喜欢明确指定。为此我创建了一系列扩展方法:
OrderByOrdinal
OrderByOrdinalDescending
ToImmutableOrdinalDictionary
(4个重载)ToOrdinalDictionary
(4个重载)ToOrdinalLookup
(2个重载)
Visual Studio中的主构造函数和记录"调用层次结构"问题
在Visual Studio中,对主构造函数和记录参数,"查找引用"(Ctrl-K, Ctrl-R)有效但"调用层次结构"(Ctrl-K, Ctrl-T)无效。虽然可以理解主构造函数参数不支持,但记录参数最终成为属性,我期望能像其他属性一样查看调用层次结构。
更令人沮丧的是无法查看"调用构造函数"的层次结构。由于类/记录声明某种程度上也充当构造函数的声明,我原以为将光标放在类/记录声明上(在名称上)会起作用。
功能需求总结
总结来说,虽然我喜欢记录类型和不可变集合,但可以通过引入以下内容减少摩擦:
- 控制生成代码中每个属性使用的相等比较器的方式
- 不可变集合的相等比较器,能指定元素比较方式
- 执行引用比较的
IEqualityComparer<T>
实现 - 显示主构造函数和记录构造函数调用的"调用层次结构"
结论
我在记录类型和集合中发现的一些问题至少某种程度上特定于我的选举网站,尽管我强烈怀疑我不是唯一在记录中使用不可变集合并希望在相等比较中使用它们的开发者。
总体而言,记录类型在网站中表现良好,我很高兴它们可用,即使仍有改进空间。同样,能自然使用不可变集合也很棒------但在执行比较时提供更多帮助会更好。