是否每个.NET 中的集合类型都应该实现所有.NET 类型接口?

  • Jonathan Allen
  • 邵思华

2015 年 1 月 27 日

话题:.NET架构

是否每个.NET 中的集合类型都应该实现所有.NET 类型接口?在 1 月 14 日进行的.NET 核心 API 审查视频中,这一问题在 API 相关的重要问题中居首位。这段视频录制了针对.NET 基础类库的十个变更请求的相关讨论。

[视频] GitHub Issue#316:为正则集合(包括 CaptureCollection、GroupCollection 和 MatchCollection)实现 IList<T>、IReadOnlyList<T> 和 IList 接口

在.NET 类库中,但凡返回集合类型的属性或方法,多数都会选择使用强类型的集合。这种集合不是诸如 IEnumerable<Foo> 或 IList<Foo> 等类型,而是强类型的 FooCollection。这种方式对于向后兼容来说更为理想,因为可以放心地在类中加入新的方法,而不必依赖于不安全的类型转换。

其实原因还不只是这一条。在.NET 1.0 版本中还没有出现泛型,意味着 IList<T> 等接口也不存在,因此只能通过创建自定义的类型来保证类型安全。

当.NET 发布之后,这些强类型中还有很大一部分没有升级为支持泛型集合接口的类型。因此微软所面对的第一个问题就是,是否应该为正则集合实现泛型接口?

第二个问题是,是否应该为只读集合实现 IList<T> 等接口?自从.NET 2.0 发布以来,由于只读集合接口的出现,IList<T> 应当只用于可变集合类型,这一问题本应迎刃而解。但由于只读集合接口相对较新,有许多 API 依然只支持 IList<T> 类型,并仅仅通过文档表示不会对其中的内容进行变更。如果某些方法需要对集合内容进行变更,理论上只需要检查一下 IsReadOnly 属性的值就可以了,但这完全取决于类库开发者和应用程序开发者的自觉性。

再回到原来的问题上,只读的正则集合是否应该支持 IList<T> 接口,以便在让不支持 IReadOnlyList<T> 接口的遗留 API 也可以调用呢?还是应当选择不支持 IList<T> 接口,以减少被某些需要可变的列表的方法所误用呢?

另一个对此问题起到影响的因素是,Windows Forms 中的数据绑定是依赖于 IList 接口的,因此如果要在用户界面中使用这些正则类型,就必须添加该接口。

结论:按照当下的设计指南的做法,为这些类型实现所有接口。从长期的考虑来看,考虑对设计指南做某些调整。

与之相关的另一个问题是,是否应该让这些类型继承于 ReadOnlyCollection<T> 类?虽然这种方式能够免去大量的模板代码,但会造成一些向后兼容的问题:

  • 重载的解析会产生变化(这个问题同样会发生在添加新接口的方案中)。
  • 使用反射的结果也会产生变化。

结论:今后,所有新的只读集合都将继承于 ReadOnlyCollection<T>,而现有的类型则保持不变。

序列化是另一个问题。由于序列化类库中的一些历史悠久的 bug 的问题,如果为类型加入对 IList 或其它接口的支持,有可能造成原本正常的序列化失败。

结论:如果造成了序列化的失败,则必须撤消该变更。

[视频] GitHub Issue#110:为 XLinq 的 doucment 和 element 加载加入 async 实现

对于这一请求的第一个问题是,应该使用方法重载还是可选参数?对于这个 API 来说,问题主要是针对 cancellation token 参数而言,但同样的问题也多次出现在其它场合中。

对于可选参数的反对意见在于它们对版本化的支持不理解。使用了可选参数之后,如果为某类型加入了一个接受更多参数的新重载,破坏重载解析的可能性就更大。

结论:可选参数值得一方式,但需要更多的指导与代码分析,以确保它们适当地处理了版本化问题。

第二个问题是,是否要加入一个支持 URI 的 LoadAsync 方法。整个团队对于该方法的同步版本颇有微词,因为它为 XLinq 类库引入了额外的依赖。

结论:如果能够让同步版本的 Load(string uri) 方法过期,那么也不需要异步的方法了。否则的话,就需要创建一个对应的异步方法,这样也可以便于将来过渡到 async 实现。

第三个问题是,是否应该为 Load 方法的所有重载都实现对应的 LoadAsync 方法?假设 cancellation token 参数依然保留的情况下,这种方法就将导致方法的数量从 4 个增加到 8 个。而如果 cancellation token 变成了可选参数,则 LoadAsync 的重载方法的数量将变成 16 个,其中的 12 个只是跳转到另外 4 个主要的重载版本。

结论:首先考虑使用场合最多的方法签名,今后再考虑加入方法重载或可选参数。

[视频] GitHub Issue#400:为 ImmutableArray<T> 类型加入 Cast<T> 和 CastFrom<TDerived> 方法

对于不可变数组来说,它们可能会遇到的转换有三种情景:

  • 静态转换为某个基础类型,这种转换必然会成功。例如将 ImmutableArray<Dog> 转换为 ImmutableArray<Animal>
  • 动态转换,这种转换有可能会失败。例如将 ImmutableArray<Animal> 转换为 ImmutableArray<Dog>。
  • 有条件地转换为某个继承类型,即大家所熟悉的“as 转换”。

目前只有最后一种转换方式已经得到了完整的支持。通过 ImmutableArray.Create 方法的某个重载可以支持静态转换,但要找到这个重载并不容易,而且这种方法是否会造成额外的内存分配不是一眼就能够看出来的。

对此问题的一个建议是,删除那个不直观的 Create 方法。添加 Cast 和 CastFrom 方法,以实现静态和动态转换操作。

对这个 API 的第一个问题是,是否应该使用 Cast 这个方法名称?主要问题在于,Cast 这个名称与 LINQ 中用于元素延迟转换的方法同名。最理想的方案是将 LINQ 中的方法名称改为 CastElements<T>,但这一点明显是不现实的。

另一个相关的问题是和 As 方法名有关的。通常来说,.NET 会使用 ToXxx 的方法名表示会造成内存分配的转换,而用 AsXxx 的方法名表现无内存分配的转换。而不可变数组中的 As 方法实际上是与 C# 中的“as”操作符具有相同的作用,而不是遵循传统意义上的做法。

另一个问题是和 Visual Basic 有关的。与 C# 不同,在 VB 中可以通过某个实例变量调用它的静态方法。虽然这种方式让人感觉不爽,但它确实已经成为了 VB 语言中的一部分,因此类似 CastFrom 这样的静态方法就让人搞不清到底是将什么转换为什么。

结论:方法名称依然有改善余地,除此之外,这个提议还是非常有用的。

[视频] GitHub Issue:#394:为 ConcurrentDictionary<TKey, TValue> 类型加入 GetOrAdd 和 AddOrUpdate 方法重载,其中包括一个 TArg 参数 factoryArgument

接下来登场的是一个 pull request 中的内容,即为 ConcurrentDictionary 加入新的方法重载。要理解这几个重载的目的,你首先必须理解闭包是如何工作的。当一个闭包产生时,必须对闭包中引用的变量分配内存。如果在一个数量巨大的循环中使用闭包,就会导致大量的内存占用。

与之相反的是,如果某个匿名方法中没有捕捉(capture)任何本地变量,那么指向该方法的委托就能够被编译器进行缓存并重用。这就使得对该方法的调用不会产生内存分配。

这些新的方法重载允许你为 GetOrAdd 和 GetOrUpdate 方法中所使用的工厂方法加入一个额外的值。一般情况下,这个额外的值足以代替闭包的使用,因此可以减少内存的占用。

这些方法也可以被实现为扩展方法,其性能和普通方法相比基本相同。因此对这个问题的讨论主要在于,该方法带来的性能优势和作为普通方法的便利性,是否能够抵消由此造成的类的体积膨胀的缺点。

一个更广泛的问题是,这种模式是否应该在整个.NET 平台中使用。几乎每个使用了委托的方法,都可以通过这一模式移除对闭包的使用。

结论:如果能够证明新的方法对性能产生了很大的改善,就决定加入这些新方法。

明天我们还将继续对这次 API审查会议的分析。

查看英文原文: Should all .NET Collections Implement all .NET Collection Interfaces?

.NET架构