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 only IEnumerable<T> is contra-variant [sic1]: In IEnumerable<T>, the type parameter T is used only in "output" positions (return values) and not in "input" positions (parameters). ICollection<T> and IList<T> use T 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.