Why generic IList<> does not inherit non-generic IList
Solution 1:
As you note, T
in IList<T>
is not covariant. As a rule of thumb: any class that can modify its state cannot be covariant. The reason is that such classes often have methods that have T
as the type of one of their parameters, e.g. void Add(T element)
. And covariant type parameters are not allowed in input positions.
Generics were added, among other reasons, to provide type safety. For example, you can't add an Elephant
to a list of Apple
. If ICollection<T>
were to extend ICollection
, then you could call ((ICollection)myApples).Add(someElephant)
without a compile-time error, as ICollection
has a method void Add(object obj)
, which seemingly allows you to add any object to the list, while in practice you can only add objects of T
. Therefore, ICollection<T>
does not extend ICollection
and IList<T>
does not extend IList
.
Anders Hejlsberg, one of the creators of C#, explains it like this:
Ideally all of the generic collection interfaces (e.g.
ICollection<T>
,IList<T>
) would inherit from their non-generic counterparts such that generic interface instances could be used both with generic and non-generic code.As it turns out, the only generic interface for which this is possible is
IEnumerable<T>
, because onlyIEnumerable<T>
is contra-variant [sic1]: InIEnumerable<T>
, the type parameterT
is used only in "output" positions (return values) and not in "input" positions (parameters).ICollection<T>
andIList<T>
useT
in both input and output positions, and those interfaces are therefore invariant.
1) IEnumerable<T>
is co-variant
Since .Net 4.5 there are the IReadOnlyCollection<out T>
and IReadOnlyList<out T>
covariant interfaces. But IList<T>
, ICollection<T>
and many of the list and collection classes don't implement or extend them. Frankly, I find them not very useful, as they only define Count
and this[int index]
.
If I could redesign .Net 4.5 from the ground up, I would have split the list interface into a read-only covariant interface IList<out T>
that includes Contains
and IndexOf
, and a mutable invariant interface IMutableList<T>
. Then you could cast IList<Apple>
to IList<object>
. I implemented this here:
M42 Collections - Covariant collections, lists and arrays.
Solution 2:
Note that since 2012, in .NET 4.5 and later, there exists a covariant (out
modifier) interface,
public interface IReadOnlyList<out T>
see its documentation.
Usual collection types like List<YourClass>
, Collection<YourClass>
and YourClass[]
do implement IReadOnlyList<YourClass>
and because of the covariance can also be used as IReadOnlyList<SomeBaseClass>
and ultimately IReadOnlyList<object>
.
As you have guessed, you will not be able to modify your list through a IReadOnlyList<>
reference.
With this new interface, you might be able to avoid the non-generic IList
all together. However you will still have the problem that IReadOnlyList<T>
is not a base interface of IList<T>
.
Solution 3:
Create an interface MyIList<T>
and let it inherit from IList<T>
and IList
:
public interface MyIList<T> : IList<T>, IList
{ }
Now create a class MySimpleList
and let it implement MyIList<T>
:
public class MySimpleList<T> : MyIList<T>
{
public int Count
{
get { throw new NotImplementedException(); }
}
public bool IsFixedSize
{
get { throw new NotImplementedException(); }
}
public bool IsReadOnly
{
get { throw new NotImplementedException(); }
}
public bool IsSynchronized
{
get { throw new NotImplementedException(); }
}
public object SyncRoot
{
get { throw new NotImplementedException(); }
}
object IList.this[int index]
{
get
{
throw new NotImplementedException();
}
set
{
throw new NotImplementedException();
}
}
public T this[int index]
{
get
{
throw new NotImplementedException();
}
set
{
throw new NotImplementedException();
}
}
public void Add(T item)
{
throw new NotImplementedException();
}
public int Add(object value)
{
throw new NotImplementedException();
}
public void Clear()
{
throw new NotImplementedException();
}
public bool Contains(T item)
{
throw new NotImplementedException();
}
public bool Contains(object value)
{
throw new NotImplementedException();
}
public void CopyTo(T[] array, int arrayIndex)
{
throw new NotImplementedException();
}
public void CopyTo(Array array, int index)
{
throw new NotImplementedException();
}
public IEnumerator<T> GetEnumerator()
{
throw new NotImplementedException();
}
IEnumerator IEnumerable.GetEnumerator()
{
throw new NotImplementedException();
}
public int IndexOf(T item)
{
throw new NotImplementedException();
}
public int IndexOf(object value)
{
throw new NotImplementedException();
}
public void Insert(int index, T item)
{
throw new NotImplementedException();
}
public void Insert(int index, object value)
{
throw new NotImplementedException();
}
public bool Remove(T item)
{
throw new NotImplementedException();
}
public void Remove(object value)
{
throw new NotImplementedException();
}
public void RemoveAt(int index)
{
throw new NotImplementedException();
}
}
What you can easily see now, is that you have to double implement a bunch of methods. One for the type T and one for object. In normal circumstances you want to avoid this. This is a problem of co-variance and contra-variance.
The best explanation you can find (for this concrete problem with IList and IList is the article from Brad already mentioned by Jon within the comments of the question.