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.
Covariance and contravariance are terms that refer to the ability to use a more derived type (more specific) or a less derived type (less specific) than originally specified. Generic type parameters support covariance and contravariance to provide greater flexibility in assigning and using generic types.
IList is invariant in its type parameter. There is no subtype relationship for lists because you can put values into a list and take values out of a list. If IList was covariant, you'd be able to add items of the wrong subtype to a given list.
Covariance in C# Covariance enables you to pass a derived type where a base type is expected. Co-variance is like variance of the same kind. The base class and other derived classes are considered to be the same kind of class that adds extra functionalities to the base type.
A type can be declared contravariant in a generic interface or delegate only if it defines the type of a method's parameters and not of a method's return type. In , ref , and out parameters must be invariant, meaning they are neither covariant nor contravariant.
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; } }
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.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With