Covariance and IList
Solution 1:
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; }
}
Solution 2:
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.
Solution 3:
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.