为什么IList<T>不仅继承自ICollection<T>?

18

有趣的是,当我在Visual Studio中查看IList<T>的定义时,它与GitHub上的源代码不同。

enter image description here

IList<T>

public interface IList<T> : ICollection<T>, IEnumerable<T>, IEnumerable
< p > < code > ICollection<T>

ICollection<T>
public interface ICollection<T> : IEnumerable<T>, IEnumerable

鉴于 ICollection<T> 已经包含了 IEnumerable<T>IEnumerable,为什么 IList<T> 需要包括它们呢?不可以简化为以下形式吗?

public interface IList<T> : ICollection<T>

我试图理解这个长接口链背后的逻辑。

您可以检查下面的源代码以查看差异。

https://github.com/microsoft/referencesource/blob/5697c29004a34d80acdaf5742d7e699022c64ecd/mscorlib/system/collections/generic/ilist.cs#L37


我猜你使用了ILSpy来反编译系统程序集,并看到IList(或<T>)列出了一些接口。这是ILSpy的一个副作用。如果一个类实现了IA并且IA暗示IB,ILSpy将会把该类列为同时实现了IA和IB。参考源代码、Github、文档,它们只列出了ICollection。 - Lasse V. Karlsen
4
“the definition” 你从哪里或者怎样得到这个定义的?我们有参考来源、github源码(针对.NET 5)、ILSpy、dotPeek、文档等多种信息来源,请告诉我们你使用的是哪一个。让我再明确一下:其中一些信息来源可能不同,而且某些信息(如ILSpy)甚至可能是错误的。 - Lasse V. Karlsen
1
@Alexander 那不是实际的源代码。那是元数据生成的代码。在 IL 中,所有接口都明确列出。记住,接口是一个接口,就像墙上插座或音频插孔一样。3针插座“继承”2针插座接口。您仍然可以看到所有的孔和引脚。组合麦克风+音频插孔“继承”2个单声道和1个麦克风引脚。您仍然可以看到所有引脚,而不是“立体声”和“麦克风”引脚。 - Panagiotis Kanavos
2
在Visual Studio中,使用Go to definition功能时,在后台使用了ILSpy,但是ILSpy会存在错误。它会强制将隐含接口添加到该类型中。您可以通过以下代码进行测试:public interface IA { } public interface IB : IA { } public class Test : IB { }然后对其进行反编译。Test类将被反编译为public class Test: IB, IA。这就是为什么我们要求提供源代码的原因。 - Lasse V. Karlsen
1
ILSpy不能给出源代码的准确副本。由于编译器进行了高级重写,因此许多结构最终会成为与原始源不同的反编译源。因此,我不认为这是ILSpy的缺陷,很可能它正按设计工作。请注意,从我的“Test”示例中,“typeof(Test).GetInterfaces()”返回“IA”和“IB”,ILDasm也是如此 - Lasse V. Karlsen
2个回答

17

简短版

.NET中,接口不形成层级树。当一个类型实现了一个派生接口时,它就实现了所有“父级”接口。这是规范的一部分。

详细版

IList为什么需要同时继承它们? 它不需要。在GitHub上.NET旧版本的实际源代码中,代码如下:

public interface IList<T> : ICollection<T>

.NET Core的源代码类似于此处

public interface IList<T> : ICollection<T>

这个问题没有解释多重继承的假设是从哪里来的。也许文档被误解了?

好的文档总是会列出类实现的所有接口。如果没有这样做,程序员将不得不追踪多个链接,才能找出一个类执行什么操作、它实现了哪些功能或者其特殊行为是什么。

事实上,COM文档在2000年左右就是这种情况,将类和接口文档分开。那时还没有Google和在线文档,所以要找出一个类执行了什么操作非常困难。几乎不可能找到需要实例化的类来获取特定服务。

Intellisense、参数信息和IDEs也会显示所有已实现的接口,因为

编辑后

因此,在代码中继承的接口是由编译器扩展的,因此存在这样的误解:

interface IX{}
interface IY:IX{}

public class C :IY{
    public void M() {
    }
}

在Sharplab.io中变成了这样

public class C : IY, IX
{
    public void M()
    {
    }
}

生成的IL显示了相同的内容:

.class public auto ansi beforefieldinit C
    extends [System.Private.CoreLib]System.Object
    implements IY,
               IX
{

这表明仅从IX继承与从所有继承的接口继承完全相同。

.NET中的接口确实是一个接口,字面上。就像墙上插座是一个接口,4针音频插孔也是一个接口一样。4针音频插孔“继承”1个立体声和1个麦克风连接。立体声连接“继承”2个单声道连接。

我们看不到2个引脚集,我们看到并使用2个单声道和1个麦克风引脚。

这在规范中有说明

在.NET中,接口确实是API规范,而不是实现。当一个类实现继承自其他接口的接口时,它将实现所有这些接口。接口不会像类那样形成层次结构树。

来自 ECMA CIL标准 接口类型派生节(1.8.9.11)

  • 对象类型形成单个继承树;接口类型不是这样。
  • 对象类型继承指定如何继承实现;必需接口不是这样,因为接口不定义实现。必需接口指定实现对象类型应支持的其他合同。

为了强调最后一个差异,请考虑一个只有一个方法的接口IFoo。从它派生的接口IBar,要求任何支持IBar的对象类型也支持IFoo。它没有说明IBar本身将具有哪些方法。


因此,在.NET Core中,IList<T>仅从ICollection<T>继承:https://github.com/dotnet/runtime/blob/release/5.0/src/libraries/System.Private.CoreLib/src/System/Collections/Generic/IList.cs - Artak

13

简而言之: 编译器将编译该类,就好像它专门实现了所有提到的接口 以及所有暗示/继承的接口 到程序集中。没有办法让 ILSpy、ILDasm 或 "Go to definition" 知道区别,除非实际下载和显示原始源代码。


由于您已经澄清您在 Visual Studio 中使用了 "Go To Definition",因此有两个相关工具:

  • ILSpy
  • ILDasm

两者采用不同的方法显示编译后程序集的内容。我相信ILSpy被用在Visual Studio的背后,但是继续阅读为什么这实际上并不重要。

如果我们在 LINQPad 中进行简单的测试:

void Main()
{
}

public interface IA
{
}

public interface IB : IA
{
}

public class Test : IB
{
}

然后要求LINQPad使用ILSpy反映代码,我们将得到这个Test的定义:

public class Test: IB, IA

显然,ILSpy显示Test同时实现了这两个接口,而源代码只用IB给出了IA

那么,ILDasm呢?我用Visual Studio编写了一个.NET 5程序集,然后使用ILDasm反编译它,与上面完全相同的代码:

.class interface public abstract auto ansi ClassLibrary3.IA
{
} // end of class ClassLibrary3.IA

.class interface public abstract auto ansi ClassLibrary3.IB
       implements ClassLibrary3.IA
{
} // end of class ClassLibrary3.IB

.class public auto ansi beforefieldinit ClassLibrary3.Test
       extends [System.Runtime]System.Object
       implements ClassLibrary3.IB,
                  ClassLibrary3.IA
{
基本上,这是编译器编译源代码的产物。我不了解足够的中间语言(IL)来知道从中间语言重新组装接口,而不提及IA是否会产生相同的输出,但我将把它留作练习。
我还查看了各种信息来源:
  1. 参考源未显式列出隐含接口
  2. Github源未显式列出隐含接口
  3. IList文档未列出隐含接口,但IList<T>文档列出了
  4. ILSpy反编译列出了所有接口
  5. ILDasm反编译列出了所有接口(这应该是实际内容,因此我认为在编译后的程序集级别无法区分)

什么是引用源?能举几个例子吗? - Shirin
@Alexander 参考源 - ProgrammingLlama
1
@Alexander .NET Core是开源的,因此源代码确实是.NET Core构建的实际源代码。对于.NET Old(即Framework),微软只发布了完成的源代码。它仍然是.NET Old构建的实际源代码,但不接受.NET Framework团队之外的问题和拉取请求。 - Panagiotis Kanavos
我知道,但由于编译后的汇编代码实际上也没有显示出任何差异,并且我们已经得到了澄清,这只是关于Visual Studio中的转到定义而已,我认为这只是额外的信息。 - Lasse V. Karlsen

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接