记录类型与集合
本文某种程度上是我在选举网站开发中使用记录类型和集合时遇到的摩擦点汇总。
记录类型回顾
这可能是本系列中最具普适价值的博文。虽然C#从10版本就引入了记录类型,但我个人使用经验有限。(尽管我期待这个特性已有十余年,不过这是题外话了。)
决定将所有数据模型设为不可变后,使用密封记录类型来实现这些C#模型几乎是顺理成章的选择。只需用主构造函数的语法指定所需属性,编译器就会自动生成大量样板代码。
以这个简单记录声明为例:
|
|
其生成的代码大致等效于:
|
|
(为简洁起见使用传统C#语法展示,实际生成代码可能使用主构造函数)
更妙的是,编译器还支持with
表达式基于现有实例创建新实例:
|
|
这一切都很美好——直到遇到特殊情况…
记录类型的相等性比较
如上所示,记录类型默认使用EqualityComparer<T>.Default
比较各属性。当属性类型的默认比较器符合需求时没问题,但ImmutableList<T>
等类型就不适用——我们大量使用了这类不可变集合。
ImmutableList<T>
未重写Equals
和GetHashCode
,因此具有引用相等语义。我实际需要的是基于元素类型的比较器,当两个不可变列表长度相同且元素按顺序对应相等时视为相等。虽然实现这种比较器很容易,但C#记录机制目前无法为单个属性指定特定比较器。
理想情况下,希望能通过特性标注指定比较器:
|
|
引用相等比较
根据数据模型设计,在单个ElectionContext
内我们只需要引用相等。创建ImmutableDictionary<Constituency, Result>
时,我希望使用仅进行引用比较的IEqualityComparer<Constituency>
,这能显著提升上下文重载时视图模型的构建效率。
.NET 5+其实提供了System.Collections.Generic.ReferenceEqualityComparer
,但由于其实现的是非泛型IEqualityComparer<object>
接口,我最初错误地忽略了它——忘了IEqualityComparer<T>
的逆变特性。
字符串序数比较扩展方法
由于通常需要使用序数比较,我创建了一系列扩展方法使意图更明确:
OrderByOrdinal
/OrderByOrdinalDescending
ToImmutableOrdinalDictionary
(4个重载)ToOrdinalDictionary
(4个重载)ToOrdinalLookup
(2个重载)
Visual Studio对记录类型的支持问题
使用"调用层次结构"(Ctrl-K, Ctrl-T)时:
- 主构造函数参数无法查看调用链
- 记录参数转换的属性也无法查看
- 查看构造函数调用需要特殊操作
功能改进建议
总结记录类型和不可变集合的使用体验,建议引入:
- 按属性控制相等比较器的机制
- 不可变集合的专用比较器
- 泛型引用相等比较器
- 完善IDE对记录类型的工具支持
结论
虽然存在这些痛点,记录类型在网站开发中表现优异,不可变集合的内置支持也很棒。期待未来版本能进一步优化这些体验。