Why doesn't IList<T> only inherit from ICollection<T>?
Short Version
In .NET, interfaces don't form hierarchy trees. When a type implements a derived interface, it implements all "parent" interfaces. That's part of the actual spec
Long Version
why does IList need to inherit from both of them
it doesn't. The actual source for .NET Old in GitHub is :
public interface IList<T> : ICollection<T>
The source for .NET Core is similar
public interface IList<T> : ICollection<T>
The question doesn't explain where the assumption for multiple inheritance comes from. Perhaps the docs were misinterpreted?
Good documentation always lists all the interfaces implemented by a class. If it didn't, programmers would have to chase down multiple links to find out what a class did, what it implemented or what the specialized behavior is.
In fact, that's how COM documentation was around 2000, separating class and interface documentation. That was before Google and online docs, so finding out what a class did was really hard. Finding out what class you needed to instantiate to get a specific service was almost impossible.
Intellisense, parameter information, IDEs display all implemented interfaces too, because
After the Edit
So the misconception arises because inherited interfaces in code are expanded by the compiler. This code:
interface IX{}
interface IY:IX{}
public class C :IY{
public void M() {
}
}
changes into this in Sharplab.io :
public class C : IY, IX
{
public void M()
{
}
}
The generated IL shows the same thing:
.class public auto ansi beforefieldinit C
extends [System.Private.CoreLib]System.Object
implements IY,
IX
{
This shows that inheriting from IX
alone is exactly the same as inheriting from all inherited interfaces.
An interface in .NET really is an interface, literally. The same way a wall socket is an interface, or a 4-pin audio jack is an interface. The 4-pin audio jack "inherits" 1 stereo and 1 mic connection. The stereo connection "inherits" 2 mono connections.
We don't see 2 pin sets though, we see and use 2 mono and 1 mic pin.
It's in the spec
In .NET, an interface really is an API specification, not an implementation. When a class implements an interface derived from others, it implements all those interfaces. Interfaces don't form hierarchy trees the way classes do.
From the Interface Type Derivation
section (1.8.9.11) of the ECMA CIL standard
- Object types form a single inheritance tree; interface types do not.
- Object type inheritance specifies how implementations are inherited; required interfaces do not, since interfaces do not define implementation. Required interfaces specify additional contracts that an implementing object type shall support.
To highlight the last difference, consider an interface, IFoo, that has a single method. An interface, IBar, which derives from it, is requiring that any object type that supports IBar also support IFoo. It does not say anything about which methods IBar itself will have.
TL;DR: The compiler will compile the class as though it specifically implements all mentioned interfaces as well as all implied/inherited interfaces into the assembly. There is no way for ILSpy, ILDasm, or "Go to definition" to know the difference without actually downloading and showing the original source code.
Since you have now clarified that you used Go To Definition in Visual Studio, there are two tools in scope:
- ILSpy
- ILDasm
Both takes different approaches to show the contents of a compiled assembly. I believe ILSpy is used behind the scenes in Visual Studio but read on for why that doesn't actually matter.
If we do a simple test in LINQPad:
void Main()
{
}
public interface IA
{
}
public interface IB : IA
{
}
public class Test : IB
{
}
and then ask LINQPad to reflect the code using ILSpy, we get this definition for Test
:
public class Test: IB, IA
Clearly ILSpy shows that Test
implements both, whereas the source just got IA
via IB
.
What about ILDasm? I wrote a .NET 5 assembly using Visual Studio, and then decompiled it using ILDasm, with the exact same code as above:
.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
{
Basically, this is an artifact of how the compiler compiles the source. I don't know enough IL to know if reassembling the interface from Intermediate Language, without mentioning IA
will actually produce the same output but I'll leave that as an excercise.
I also took a look at various sources for this information:
- Reference source does not explicitly list implied interfaces
- Github source does not explicitly list implied interfaces
- Documentation for
IList
does not but forIList<T>
does - ILSpy decompiles listing all interfaces
- ILDasm decompiles listing all interfaces (and this is supposed to be the actual contents so I'd say there is no way to tell the difference at the compiled assembly level)