写点什么

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

2011 年 11 月 18 日

.NET 4.5 中添加了两个新的集合接口,IReadOnlyList 和 IReadOnlyDictionary。尽管这些接口表面上看起来是如此稀松平常,但是它们却暴露出关于向后兼容性、互操作性、以及协变的作用等相当复杂的故事。

IReadOnlyList 和 IReadOnlyDictionary 是.NET 开发者一直都想得到的两个接口。一个只读接口除了提供某种(相对于可写入接口的)对称感之外,还应消除实现那些只抛出 NotSupportedException 异常而什么都不做的方法。由于时间原因,这一切未能完成。

接下来一次机会是在.NET 2.0 中引入泛型。这使得微软可以淘汰弱类型集合,并使用强类型的对等集合替代之。但是,基类库 [1] 团队又一次错过了这次提供只读列表(read-only list)的机会,正如 Kit George 所写到的

由于我们打算为你与 Joe 所谈论的问题提供一种缺省实现,而不是给出一个接口,因此我们提供了 ReadOnlyCollectionBase 基类。然而,我能理解人们之所以不愿使用它,是因为它不是强类型的。但随着泛型的引入,我们现在还拥有了 ReadOnlyCollection,这样你不仅获得了同等功能,而且还是强类型的:太棒了!

由于 ReadOnlyCollection不是密封类,因此必要时你可以开足马力随意编写你自己的集合。因为我们为此创作的这些集合可适应一般需求,所以我们尚未计划为与此相同的概念引入接口。

Krzysztof Cwalina 也对此主题发表了看法,

无论这听起来是否令人惊讶,但是 IList 和 IList是我们打算用于只读集合的两个接口。它们都拥有 IsReadOnly 布尔型属性,当某个只读集合实现此属性后应返回 true。我们不想添加纯只读接口的原因是,我们觉得它会给基类库添加太多不必要的复杂性。请注意,就复杂性而言,我们既指此新接口又指其消费者。

我们认为,如果 API 设计者不关心在运行时检查 IsReadOnly 属性、及其可能抛出的异常,那么这种情况下使用 IList 接口并无大碍;如果他们愿意一次性地提供一个真正干净的自定义 API,那么这种情况下他们应显示实现 IList 接口、并公布量身定制的只读 API。对于从对象模型中公开的集合而言,后者是种典型方式。

虽然开发者曾抱怨此种情况,但是泛型所提供的新机会远远大于这个症结,并且该问题在.NET 4 以前很大程度上被忽视了。然而,此决定也引发了一些反应,我们会在稍后讨论。

随着在.NET 4 中一个令人兴奋的新功能被添加到运行时。在较早版本的.NET 中,当接口成为类型时,那些接口是受到过分限制的。例如,即使 Customer 继承自 Person,也无法将类型为 IEnumerable的对象作为参数传给某个参数类型为 IEnumerable的函数。随着协变支持的添加,该限制才得以部分解除。

我们之所以说“部分”,是因为在某些情况下,人们应一次性地使用某个具有丰富API 的接口,而不是使用IEnumerable 接口。即使IList 接口不是协变的,一个只读列表接口也理应如此。不幸的是,.NET 基类库团队再次决定不解决此疏忽。

然后,WinRT 的引入和COM 的卷土重来改变了一切。COM 互操作性曾是开发者在别无其他选择的情况下才会使用的一种技术,但现已成为.NET 编程的基石。而且由于WinRT 公开了 IVectorView IMapView<K, V> 接口,因此.NET 也必须作出相应调整。

WinRT 计划中一个颇为有趣的功能是,为每个开发平台公布不同但功能类似的 API。正如你可能已经知道的,通过 JavaScript 开发者眼睛看到的是,所有方法名都表示为驼峰式大小写(camelCased[2]),而 C++ 和.NET 开发者看到的方法名则表示为帕斯卡大小写(PascalCased[3])。另一处更加剧烈的变化是,在 C++ 与.NET 的接口之间实现自动映射。因此.NET 开发者无需处理 Windows.Foundation.Collections 命名空间,只要继续使用 System.Collections.Generic 命名空间就行了。IVectorView和 IMapView<K, V> 这两个接口会被运行库分别转化为 IReadOnlyList接口和 IReadOnlyDictionary<TKey, TValue> 接口。

值得注意的是,在 C++/WinRT 中的这些接口名在某定程度上是更准确的。这些接口是用来表示针对某集合的一些视图,但是接口并不确保该集合本身是不变的。即使在那些经验丰富的.NET 开发者中也很常见的一种错误是,假设 ReadOnlyCollection 类型是某个不变集合副本,其实,它只是对某个活动集合的包装(wrapper)(关于只读、冻结、且不变集合的详细信息,请参阅Andrew Arnott 的同名帖子)。

尽管IList接口与IReadOnlyList接口具有全部相同的成员、并且所有IList类型的列表都可表示为只读列表,但是IList不是继承自IReadOnlyList,当有人得知这些以后可能会觉得很有趣。 Immo Landwerth 解释说

之所以能工作是因为那些只读接口是可读写接口的纯子集,这看起来是个合理的假设。不幸的是,此假设与实际不符,因为在元数据级别上位于每个接口上的每个方法都有各自的槽(slot)(这使得显式接口实现得以工作)。

或者换言之,引入只读接口作为某些可变种类基类的唯一机会是退回到.NET 2.0,即它们最初被构思出来的时候。一旦全面推出,对其能做的唯一改变就是添加协变和 / 或逆变标记(在 VB 和 C#中表示为“in”和“out”)。

当被问及为什么没有 IReadOnlyCollection接口时,Immo 回答说,

我们曾考虑过这个设计,但是我们觉得加入一个提供仅有 Count 属性的类型并不会为基类库增加太多价值。在基类库团队中,我们认为,如果某个 API 是从负 1000 点开始的,那么即使能提供一些价值也不足以证明可被添加。添加新 API 的理由还包括代价,例如,开发者会拥有更多可供选择的概念。起初我们认为,添加这个类型将使得代码在某些场景(你只想获得计数,然后对它做一些有趣的东西)下获得更好的性能。例如,批量添加到某个现有集合。然而,在这些场景下,我们已鼓励人们仅采用 IEnumerable接口,而且对于拥有实现了 ICollection接口的实例的特殊情况也是如此。自从我们所有的内建集合类型实现了此接口以后,在那些最常见场景下并未获得任何性能收益。顺便说一下,针对 IEnumerable的扩展方法 Count() 同样可以完成此功能。

这些新接口可用于.NET 4.5 和.NET for Windows 8。

译注

[1] 基类库,Base Class Library,缩写为 BCL。有关基类库的更多信息,请参与 MSDN

[2] camelCased,驼峰式命名法,又称小驼峰式命名法(lower camel case)。格式为,第一个单字以小写字母开始;第二个单字的首字母大写,例如:firstName、lastName。

[3] PascalCased,帕斯卡命名法,又称大驼峰式命名法(upper camel case)。格式为,每一个单字的首字母都采用大写字母,例如:FirstName、LastName、CamelCase。

查看英文原文: The Story of Read-Only Collection Interfaces in .NET

2011 年 11 月 18 日 04:201566
用户头像

发布了 55 篇内容, 共 16.0 次阅读, 收获喜欢 0 次。

关注

评论

发布
暂无评论
发现更多内容

技术博客,从零到数万访问,这两年我都做了什么

android 博客 经验分享

天啊!怎么会有人把Spring Cloud微服务架构讲得这么透彻?

Java成神之路

Java 程序员 架构 面试 编程语言

移动端技术方案设计的经验总结

张明云

android 架构 移动应用 架构师 技术方案

架构词典:SLA

lidaobing

架构 SLA

Flutter Plugin插件开发填坑指南

flutter 经验分享

架构师训练营第 1 期 - 第十一周作业

Todd-Lee

极客大学架构师训练营

Gemini双子新约软件系统开发|Gemini双子新约APP开发

开發I852946OIIO

系统开发

开源软件联盟PostgreSQL分会投稿指南

PostgreSQLChina

数据库 postgresql 软件 投稿

二本毕业、两年Javacrud经验,面试阿里侥幸通过成功拿到P6级offer,分享面经!

Java成神之路

Java 程序员 架构 面试 编程语言

史上最优美的Android原生UI框架XUI使用指南

android UI 框架开发

网络入门模拟器:Cisco Packet Tracer

网络技术平台

第11周学习总结

饭桶

我把Github上最牛b的Java教程和实战项目整合成了一个PDF文档

Java成神之路

Java 程序员 架构 面试 编程语言

一文搞懂RESTful API

bigsai

RESTful Rest

史上最实用的Android切片应用库XAOP使用指南

android aop 开源项目 框架

字节总监首发1121道LeetCode算法刷题笔记(含答案)

Crud的程序员

程序员 面试 算法 字节 面试刷题

从构建小系统到架构分布式大系统,Spring Boot2的精髓全在这里了

Java成神之路

Java 程序员 架构 面试 编程语言

Java对IPv6的支持详解:支持情况、相关API、演示代码等

JackJiang

Java 网络编程 ipv6 ipv4

深入理解Git的实现原理

程序员小灰

c++ git Linux 项目管理 架构师

我是因为这个才选择当程序员的,那么你呢?

Java架构师迁哥

京东T8Java架构师总结整理的《15w字的Java面试手册》,涵盖了大厂所有主流技术面试题及答案!

Java成神之路

Java 程序员 架构 面试 编程语言

史上最好用的Android全量版本更新库XUpdate使用指南

android UI 框架开发 xupdate

roblox 杂记

katichar

第十一周课后练习

饭桶

如何在高速发展中等一等老人 银行数字化服务显温度

CECBC区块链专委会

银行 养老服务

腾讯云区块链总经理李力:产业区块链的四大发展趋势

CECBC区块链专委会

区块链 大数据

话题讨论 | 那些年奇葩的面试经历

三号无名指

话题讨论

详解TCP IP网络协议栈底层原理到徒手实现

赖猫

c++ Linux 编程 程序 网络协议栈

为什么说区块链完全去中心化做不到且没有意义

CECBC区块链专委会

区块链 去中心化

史上最全的开源项目创作指南

开源 经验分享

default-servlet-handler不生效原因,springmvc静态资源拦截方案比较

叫练

springmvc 静态资源拦截 default-servlet-handler 资源配置不生效

微服务架构下如何保证事务的一致性

微服务架构下如何保证事务的一致性

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