54

I would like a Covariant collection whose items can be retrieved by index. IEnumerable is the only .net collection that I'm aware of that is Covariant, but it does not have this index support.

Specifically, I'd like to do this:

List<Dog> dogs = new List<Dog>();

IEnumerable<Animal> animals = dogs;
IList<Animal> animalList = dogs; // This line does not compile

Now, I'm aware of why this is a problem. List implements ICollection that has an Add method. By up casting to IList of Animals, it would allow subsequent code to add any type of animal which is not allowed in the "real" List<Dog> collection.

So is anyone aware of a collection that supports index lookups that is also covariant? I would like to not create my own.

Alexei Levenkov
  • 98,904
  • 14
  • 127
  • 179
Brian M
  • 553
  • 1
  • 4
  • 6
  • 7
    +1 … the lack of read-only collection interfaces in .NET (apart from `IEnumerable`) makes this pretty much impossible but I imagine this is a common use-case, perhaps someone has come up with a feasible solution after all. – Konrad Rudolph Apr 29 '11 at 12:44
  • You can use IEnumerable<> together with ElementAt(), though the syntax wont be as pretty. – Felix Ungman Apr 29 '11 at 12:50

4 Answers4

65

Update: from .NET 4.5 onwards there is IReadOnlyList<out T> and IReadOnlyCollection<out T> which are both covariant; The latter is basically IEnumerable<out T> plus Count; the former adds T this[int index] {get;}. It should also be noted that IEnumerable<out T> is covariant from .NET 4.0 onwards.

Both List<T> and ReadOnlyCollection<T> (via List<T>.AsReadOnly()) implement both of these.


It can only be covariant if it only has a get indexer, i.e.

public T this[int index] { get; }

But all main collections have {get;set;}, which makes that awkward. I'm not aware of any that would suffice there, but you could wrap it, i.e. write an extension method:

var covariant = list.AsCovariant();

which is a wrapper around an IList<T> that only exposes the IEnumerable<T> and the get indexer...? should be only a few minutes work...

public static class Covariance
{
    public static IIndexedEnumerable<T> AsCovariant<T>(this IList<T> tail)
    {
        return new CovariantList<T>(tail);
    }
    private class CovariantList<T> : IIndexedEnumerable<T>
    {
        private readonly IList<T> tail;
        public CovariantList(IList<T> tail)
        {
            this.tail = tail;
        }
        public T this[int index] { get { return tail[index]; } }
        public IEnumerator<T> GetEnumerator() { return tail.GetEnumerator();}
        IEnumerator IEnumerable.GetEnumerator() { return tail.GetEnumerator(); }
        public int Count { get { return tail.Count; } }
    }
}
public interface IIndexedEnumerable<out T> : IEnumerable<T>
{
    T this[int index] { get; }
    int Count { get; }
}
Marc Gravell
  • 1,026,079
  • 266
  • 2,566
  • 2,900
  • Thanks. This seems the best way forward. – Brian M Apr 29 '11 at 16:22
  • 2
    I've sometimes wished Microsoft had managed a way to have IList inherit from sub-interfaces IReadableByIndex (which you call IIndexedEnumerable), IWritableByIndex, and iAppendable, so as to allow useful covariance and contravariance. Unfortunately, there's as yet no way for a read-only or write-only property to be implemented by a read-write property. If that could be done, all the members of the covariant/contravariant sub-interfaces would naturally be implemented by any valid implementation of IList, so the sub-interfaces could be added without breaking existing code. – supercat Jun 14 '11 at 20:52
  • Given that such a thing isn't possible, your approach is perhaps the best way forward. I guess before the advent of covariance the all-in-one IList may not have been too bad, but the necessity of having a read-write indexer rather than a read indexer and a write indexer makes co/contra-variance useless for most collections. BTW, I wonder why something with a read-only property and write-only property but no read-write indexer can't be nicely read and written? Why can't a compiler select the best property that will actually work? – supercat Jun 14 '11 at 21:02
  • Awesome answer. Just wanted to point out that despite IEnumerable became covariant from .NET 4.0 onwards, IList still remains invariant, as of today's .NET Standard 2.1, so Microsoft forces us to stick with IReadOnlyList. – Lesair Valmont Mar 27 '21 at 22:45
  • @Lesair that's not oversight; that's because `IList` *cannot* be covariant (or contravariant), as the API surface (which allows you to put values in and take them out) is not compatible with the demands of variance. So it isn't Microsoft who is forcing that; it is the nature of `IList` – Marc Gravell Mar 28 '21 at 10:11
9

Here's a class I wrote to address this scenario:

public class CovariantIListAdapter<TBase, TDerived> : IList<TBase>
    where TDerived : TBase
{
    private IList<TDerived> source;

    public CovariantIListAdapter(IList<TDerived> source)
    {
        this.source = source;
    }

    public IEnumerator<TBase> GetEnumerator()
    {
        foreach (var item in source)
            yield return item;
    }

    IEnumerator IEnumerable.GetEnumerator()
    {
        return GetEnumerator();
    }

    public void Add(TBase item)
    {
        source.Add((TDerived) item);
    }

    public void Clear()
    {
        source.Clear();
    }

    public bool Contains(TBase item)
    {
        return source.Contains((TDerived) item);
    }

    public void CopyTo(TBase[] array, int arrayIndex)
    {
        foreach (var item in source)
            array[arrayIndex++] = item;
    }

    public bool Remove(TBase item)
    {
        return source.Remove((TDerived) item);
    }

    public int Count
    {
        get { return source.Count; }
    }

    public bool IsReadOnly
    {
        get { return source.IsReadOnly; }
    }

    public int IndexOf(TBase item)
    {
        return source.IndexOf((TDerived) item);
    }

    public void Insert(int index, TBase item)
    {
        source.Insert(index, (TDerived) item);
    }

    public void RemoveAt(int index)
    {
        source.RemoveAt(index);
    }

    public TBase this[int index]
    {
        get { return source[index]; }
        set { source[index] = (TDerived) value; }
    }
}

Now you can write code like this:

List<Dog> dogs = new List<Dog>();
dogs.Add(new Dog { Name = "Spot", MaximumBarkDecibals = 110 });

IEnumerable<Animal> animals = dogs;
IList<Animal> animalList = new CovariantIListAdapter<Animal, Dog>(dogs);

animalList.Add(new Dog { Name = "Fluffy", MaximumBarkDecibals = 120 });

The changes are visible in both lists, because there's really still only 1 list. The adapter class just passes the calls through, casting items as necessary to achieve the desired IList<TBase> interface.

Obviously, if you add anything but Dogs to animalList, it will throw an exception, but this met my needs.

G-Mac
  • 1,173
  • 13
  • 10
  • 1
    you don't need to create a new adapter for this. Just create a new list. like this: new List(dogs) and everything will work fine. You can though this wouldn't solve contra-variance problem. – Shoaib Shakeel Jul 17 '15 at 18:47
  • It is polymorphism, even you added an object, different as Animal. this link https://msdn.microsoft.com/en-us/library/ee207183.aspx describe about Covariance and Contravariance – natnael88 Jan 29 '16 at 15:50
  • in the same pattern you can write a ContravarianceIListAdapter. A recommendation: Write extension methods, that hide these adapters, so that you can write dogs.AsList() – Felix Keil Sep 12 '18 at 08:27
5

Technically, there's the array collection. It's sort of broken in its variance, but it does what you ask.

IList<Animal> animals;
List<Dog> dogs = new List<Dog>();
animals = dogs.ToArray();

You will, of course, blow up rather spectacularly at runtime if you try to put a Tiger in the array anywhere.

Anthony Pegram
  • 123,721
  • 27
  • 225
  • 246
4

As of .NET Framework 4.5, there exists an interface IReadOnlyList which is covariant. It is essentially the same as the IIndexedEnumerable interface in Mark Gravell's answer.

IReadOnlyList is implemented like this:

  /// <summary>
  /// Represents a read-only collection of elements that can be accessed by index.
  /// </summary>
  /// <typeparam name="T">The type of elements in the read-only list. This type parameter is covariant. That is, you can use either the type you specified or any type that is more derived. For more information about covariance and contravariance, see Covariance and Contravariance in Generics.</typeparam>
    public interface IReadOnlyList<out T> : IReadOnlyCollection<T>, IEnumerable<T>, IEnumerable
      {
        /// <summary>
        /// Gets the element at the specified index in the read-only list.
        /// </summary>
        /// 
        /// <returns>
        /// The element at the specified index in the read-only list.
        /// </returns>
        /// <param name="index">The zero-based index of the element to get. </param>
        T this[int index] { get; }
      }
Tongfei Chen
  • 613
  • 4
  • 14