[翻译]List<T>为什么实现了那么多接口?
原文地址:http://blogs.msdn.com/b/ericlippert/archive/2011/04/04/so-many-interfaces.aspx
原作者:Eric Lippert
Eric Lippert是微软员工,C#编译器的主要开发人员之一。
今天,我在StackOverflow上回答了一个问题。按照以往的习惯,我把它以对话体的形式整理成一篇博客。
MSDN的文档中说List<T>是这样声明的:
public class List : IList<T>, ICollection<T>, IEnumerable<T>, IList, ICollection, IEnumerable
那么List真的实现了这么多接口吗?
是的。
为什么会有这么多接口呢?
比如说IList<T>派生自IEnumerable<T>,那么派生程度较高的接口的实现者就必须要实现派生程度较低的接口。接口继承就是这样的;如果你要满足派生程度较高的类型的契约的话,你就必须要满足派生程度较低的类型。
那么就是说一个class或者一个struct必须要实现其传递闭包上所有接口定义的所有方法吗?
非常正确。
一个实现了派生程度较高的接口的class或者struct必须要在其基类型列表中声明它实现了那些派生程度较低的接口吗?
不是。
那会被禁止声明吗?
不会。
那么就是说派生程度较低的接口是否被声明在基类列表中是可选的了?
是的。
总是这样的吗?
基本上总是这样的:
interface I1 {}
interface I2 : I1 {}
interface I3 : I2 {}
I3是否显式指明它实现了I1是可选的。
class B : I3 {}
I3的实现者必须实现I2和I1,但不一定要显式指明。是否指明是可选的。
class D : B {}
派生类可以指明它实现了其基类的接口,但是并不是一定要这么做。
下面有一个例子:
class C<T> where T : I3
{
public virtual void M<U>() where U : I3 {}
}
T和U所对应的类型参数必须要实现I2和I1,但是T的U的泛型类型约束中却不一定要显式指明出来。
分布类中是否重新声明它所实现的接口也是可选的:
partial class E : I3 {}
partial class E {}
类型E的第二部分可以声明它实现了I3,I2或I1,但并不是一定要这么做。
好吧,我懂了;这是可选的。那怎么会有人想去声明一个并不是一定要声明的接口呢?
可能是因为他们认为这么做会让代码更易读更具有自说明性吧。
有可能某个开发者会写出如下的代码:
interface I1 {}
interface I2 {}
interface I3 : I1, I2 {}
然后他突然发觉I2应该继承自I1。干嘛非得要求开发者在把I2修改为继承自I1之后再回头去在I3的声明中删除掉对I1的继承呢?我找不到任何理由去强制开发人员删除掉冗余的声明信息。
除了易读性和易懂性之外,在基类列表中显式声明一个接口和虽不声明但实现这个接口会有什么技术上的区别吗?
通常没有,但是有一种情况下会有一点细微的差别。假设有个类型D,它的基类B实现了某些接口。D通过B也就自动实现了那些接口。如果你在D的基类列表中显式声明了这些接口的话,那么C#编译器将会做一次接口重新实现。其细节有些隐晦;如果你对此有兴趣的话那我建议你去仔细阅读C# 4语言规范的13.4.6。基本上来说,编译器会“重新开始”并找出哪个方法实现了哪个接口。
List<T>的源代码中真的显式声明了那么多接口吗?
不是的。真正的源代码是这样的:
public class List<T> : IList<T>, System.Collections.IList
为什么虽然源代码没有显式声明所有接口,但是MSDN还是把所有的接口都列出来了呢?
因为MSDN是文档,它应该提供尽可能多的信息。在一页文档中给出完整的信息总比让你去翻阅十页文档才能找到一个类型实现的全部接口好一些。
为什么有些工具,比如说Reflector或者对象浏览器会把所有接口都显示出来呢?
这些工具不知道源代码是什么样子的。它们只能从元数据入手。因为显式声明全部接口只是可选的,所以这些工具根本就无从得知源代码到底有没有显式声明所有接口。反正怎么显示都有可能是错的,那这些工具还不如就把信息罗列的全面一些。这些工具给你显示了有可能比实际情况更多的信息,而不是向你隐藏一些你有可能会需要的信息是因为它们想要帮助你。
我发现IEnumerable<T>继承自IEnumerable,但是IList<T>并没有继承自IList,这是怎么回事呢?
这和IEnumerable<T>是协变的而IList<T>不是协变的是一个原因。通过装箱,我们可以把一个整数序列当做一个object的序列来对待。但是一个可读可写的整数列表却不可以被当做一个可读可写的object的列表来对待,因为你有可能给可读可写的object列表中加入一个String。一个实现IEnumerable<T>的类型可以很容易的满足IEnumerable,只要加一个装箱的helper方法就行了。但是实现IList<T>的类型却不一定能够满足IList,所以IList没有继承自IList。
那为什么List<T>又去实现了IList呢?
这个有点奇怪,因为除了T为object时之外,其他情况下List<T>都不满足的IList的要求。这有可能是因为想要给升级老的C# 1.0代码的人行个方便吧,好让他们更容易的用到泛型。那些想要升级代码的人可能已经确保了只向列表中加入正确类型的对象。而且多数情况下,当你把一个IList当做参数传递出去的时候,被调用的方法也只是会去按索引访问列表,而不是去向列表中加入一些任意类型的对象。