.NET中只读集合接口的故事

jopen 11年前
     <p> .NET 4.5 中添加了两个新的集合接口,IReadOnlyList 和 IReadOnlyDictionary。尽管这些接口表面上看起来是如此稀松平常,但是他们却揭露了与向后兼容性、互操作性、以及协变的作用等有关的相当复杂的故事。</p>    <p> IReadOnlyList 和 IReadOnlyDictionary 是 .NET 开发者自始至终都想得到的接口。只读接口除了提供某种对称性之外,还应消除那些什么都不做而只抛出 NotSupportedException 异常的方法。由于某些随时间流逝已不可知的原因,此接口并未完成。</p>    <p> 接下来一次机会是在 .NET 2.0 中引入泛型。这使得微软可以淘汰弱类型集合,并使用强类型变体替代它们。基类库<sup>[1]</sup>团队再次错过了这个提供只读列表(read-only list)的机会,正如 <a href="/misc/goto?guid=4958200165604505087">Kit George 所写的那样</a>,</p>    <blockquote>     <p>因为对于你与 Joe 所谈论的问题,我们打算提供一种缺省实现,而不是给你一个接口,所以我们提供了 ReadOnlyCollectionBase 基类。然而,鉴于它不是强类型的,我能理解人们不愿使用它的原因。但随着泛型的引入,我们现在同时拥有了 ReadOnlyCollection<T>,所以你不仅获得了同等的功能,而且就是强类型的:太棒了!</p>     <p>ReadOnlyCollection<T>不是密封类,因此如果需要可以随意在此之上编写你自己的集合。自从我们为此创作的这些集合可适合一般需求以来,我们尚没有计划为与此相同的概念引入接口。</p>    </blockquote>    <p> Krzysztof Cwalina 也对此主题进行了评论,</p>    <blockquote>     <p>无论这听起来令人惊讶与否,但是 IList 和 IList<T>是我们所期望的只读集合接口。它们都拥有 IsReadOnly 布尔型属性,当某个只读集合实现此属性后应返回 true。我们不想添加纯粹的只读接口的原因是,我们觉得它会给基类库添加太多不必要的复杂性。请注意,就复杂性而言,我们既指此新接口又指其消费者。</p>     <p>我们觉得 API 的设计者们要么是并不关心在运行时检查 IsReadOnly 属性、及其可能抛出的异常,在这种情况下使用 IList 接口就不错,要么他们愿意提供一个真正整洁的自定义 API,在这种情况下他们应显示实现 IList 接口、并公布自定义的简洁的只读 API。对于从对象模型中公开的集合而言,后者是种典型方式。</p>    </blockquote>    <p> 尽管开发曾抱怨此种情况,由于泛型所提供的新机会远远大于这个症结,因此该问题在 .NET 4 以前很大程度上被忽视了。然而,此决定也引发了一些反响,我们将在稍后讨论。</p>    <p> 随着在 .NET 4 中一个令人兴奋的新功能被添加到运行时。当早期版本的 .NET 接口出现在类型中时,那些接口是被过度限制的。例如,即使 Customer 继承自 Person,也无法将类型为 IEnumerable<Customer>的对象作为参数类型为 IEnumerable<Person>的函数的参数使用。随着<a href="/misc/goto?guid=4958200166352298176">协变支持</a>的添加,该限制才得以部分解除。</p>    <p> 我们之所以说“部分”,是因为在某些情景下,相对于 IEnumerable 接口而言,人们更愿意使用一个具有丰富 API 的接口。而且当 IList 接口不支持协变的时候,至少该有一个只读列表接口。不幸的是,.NET 基类库团队再次决定不解决这个疏忽。</p>    <p> 接着,WinRT 的引入和 COM 的死灰复燃改变了一切。COM 互操作性曾是开发者在别无选择的情况下才使用的一种技术,但现已成为 .NET 编程的基石。而且由于 WinRT 公开了 <a href="/misc/goto?guid=4958200167086534346">IVectorView<T></a>和 <a href="/misc/goto?guid=4958200167825775192">IMapView<K, V></a>接口,因此 .NET 必须与时俱进。</p>    <p> WinRT 计划中一个颇为有趣的功能是,为每个开发平台公布不同但功能类似的 API。正如你可能已经知道的,通过 JavaScript 开发者的眼睛所看到的是,所有方法名都是驼峰式大小写(camelCased<sup>[2]</sup>)表示的,而 C++ 和 .NET 开发者所看到方法则是以帕斯卡大小写(PascalCased<sup>[3]</sup>)表示的。另一处更加剧烈的变化是,在 C++ 与 .NET 的接口之间实现自动映射。因此 .NET 开发者无需处理 Windows.Foundation.Collections 命名空间,而是继续使用 System.Collections.Generic 命名空间。IVectorView<T>和 IMapView<K, V>这两个接口会被运行库转化为 <a href="/misc/goto?guid=4958200168570428482">IReadOnlyList<T></a>和 <a href="/misc/goto?guid=4958200169312101336">IReadOnlyDictionary<TKey, TValue></a>。</p>    <p> 值得注意的是,在C++/WinRT 中的这些接口名在某定程度上是更准确的。这些接口是用来表示针对某集合的一些视图,但是接口并不确保该集合本身是不可变的。即使在那些经验丰富的 .NET 开发者中也很常见的一种错误是,假设 ReadOnlyCollection 类型的对象是某个集合的不可变副本,其实,事实上此对象仅仅是对某活动集合的包装(wrapper)(关于<a href="/misc/goto?guid=4958200170053829111">只读、冻结、且不可变集合</a>的详细信息,请参阅 Andrew Arnott 的同名帖子)。</p>    <p> 当得知尽管 IList<T>接口具有与 IReadOnlyList<T>接口所有相同的成员、并且所有列表都可表示为只读列表,而 IList<T>却不是继承自 IReadOnlyList<T>以后,有人可能会觉得很有趣。<a href="/misc/goto?guid=4958200170779526619">Immo Landwerth 解释说</a>,</p>    <blockquote>     <p>这看起来是个合理的假设,它之所以能工作是因为那些只读接口是可读写接口的纯粹子集。不幸的是,此假设与预期不符,因为在元数据级别上位于每个接口上的每个方法都有其自己的槽(这使得显式接口实现得以工作)。</p>    </blockquote>    <p> 或者换言之,他们必须将只读接口作为那些可变种类的基类引入的唯一机会就是退回到 .NET 2.0,即它们最初被构思出来的时候。一旦放虎归山,对其能做的唯一改变就是添加协变和/或逆变标记(在 VB 和 C# 中表示为“in”和“out”)。</p>    <p> 当被问及为什么没有 IReadOnlyCollection<T>接口时,Immo 回答说,</p>    <blockquote>     <p>我们曾考虑过这个设计,但是我们觉得加入一个提供仅有 Count 属性的类型对于基类库而言不会增加很多价值。在基类库团队中,我们认为,如果一个 API 从负1000点开始,那么即使能提供一些价值也不足以证明可被添加。添加新 API 的理由也包括成本,例如,开发者会拥有更多可供选择的概念。起初我们认为,添加这个类型将使得代码在某些场景(你只想获得计数,然后对它做一些有趣的东西)下获得更好的性能。例如,批量添加到现有集合。然而,在这些场景下,我们鼓励人们仅采用一个 IEnumerable<T>接口,而且对于拥有实现了 ICollection<T>接口的实例的特殊情况也是如此。自从所有我们的内建集合类型实现了此接口之后,然而在那些最常见的情况下并未没有获得任何性能收益。顺便说一下,针对 IEnumerable<T>的扩展方法 Count ()同样可以完成此功能。</p>    </blockquote>    <p> 这些新接口可用于 .NET 4.5 和 .NET for Windows 8。</p>    <p> <strong>译注</strong></p>    <p> [1] <strong>基类库</strong>,Base Class Library,缩写为 BCL。有关基类库的更多信息,请参与 <a href="/misc/goto?guid=4958200171516965859" target="_blank">MSDN</a>。</p>    <p> [2] <strong>camelCased</strong>,驼峰式命名法,又称小驼峰式命名法(lower camel case)。格式为,第一个单字以小写字母开始;第二个单字的首字母大写,例如:firstName、lastName。</p>    <p> [3] <strong>PascalCased</strong>,帕斯卡命名法,又称大驼峰式命名法(upper camel case)。格式为,每一个单字的首字母都采用大写字母,例如:FirstName、LastName、CamelCase。</p>    <p> <strong>查看英文原文:</strong><a href="/misc/goto?guid=4958200172256418193">The Story of Read-Only Collection Interfaces in .NET</a><br />       来自: <a id="link_source2" href="/misc/goto?guid=4958200172999587627" target="_blank">InfoQ</a></p>