Thursday, 12 July 2012

IList Covariance

In this post I am going to describe a way to achieve Covariance for an IList<> implementation.

Now, this is obviously not something that can be done in a Type safe way (otherwise it would already be available out of the box).

There are plenty of details on why this is the case. But it boils down to this:

You have a IList<T> such that you want to add to the list, but the T here is a superclass. How can the compiler know that the actual underlying Type is the one that you are adding? It can’t.

Now, what if you promise to be really good and only use it carefully? Well, in that case, there are plenty of solutions out there. One that caught my eye was this one from Stack Overflow. And I have taken the liberty to enhance this with a few methods that IList implements.

/// <summary>
/// Covariant Enumerable with Index and Count properties
/// </summary>
/// <typeparam name="T"></typeparam>
public interface IIndexedEnumerable<out T> : IEnumerable<T>
{
/// <summary>
/// Gets the <see cref="T"/> at the specified index.
/// </summary>
T this[int index] { get; }

/// <summary>
/// Gets the count.
/// </summary>
int Count { get; }
}

public static class ListCovarianceExtensions
{
/// <summary>
/// Converts a list into a covariant enumerable with Index and Count properties.
/// </summary>
/// <typeparam name="T">The base type for the covariance</typeparam>
/// <param name="list">The list.</param>
/// <returns>A covariant enumerable with Index and Count properties.</returns>
public static IIndexedEnumerable<T> AsCovariant<T>(this IList<T> list)
{
return new CovariantList<T>(list);
}

/// <summary>
/// Adds the item to the specified indexed enumerable.
/// </summary>
/// <typeparam name="T">The covariant type that the enumerable holds.</typeparam>
/// <param name="list">The enumerable list.</param>
/// <param name="item">The item to add.</param>
/// <remarks>
/// Covariance clearly doesn't let you add items to a generic list, so we use a unsafe Add method. The type check is still performed, but this is not a type safe Add and should be used with caution.
/// </remarks>
/// <exception cref="ArgumentException">Thrown when the underlying list type doesn't match the item type.</exception>
/// <returns>the enumerable</returns>
public static IIndexedEnumerable<T> Add<T>(this IIndexedEnumerable<T> list, T item)
{
var unsafeList = ((IUnsafeList)list);

if (item.GetType() != unsafeList.ListItemType)
{
throw new ArgumentException("item", string.Format("The type of the item to add is {0}, but the underlying list has a type of {1}", item.GetType(), unsafeList.ListItemType));
}

unsafeList.List.Add(item);
return list;
}

/// <summary>
/// Adds the item to the specified indexed enumerable at the specified index.
/// </summary>
/// <typeparam name="T">The covariant type that the enumerable holds.</typeparam>
/// <param name="list">The enumerable list.</param>
/// <param name="item">The item to add.</param>
/// <param name="index">The index.</param>
/// <exception cref="ArgumentException">Thrown when the underlying list type doesn't match the item type.</exception>
/// <returns>the enumerable</returns>
public static IIndexedEnumerable<T> Insert<T>(this IIndexedEnumerable<T> list, T item, int index)
{
var unsafeList = ((IUnsafeList)list);

if (item.GetType() != unsafeList.ListItemType)
{
throw new ArgumentException("item", string.Format("The type of the item to add is {0}, but the underlying list has a type of {1}", item.GetType(), unsafeList.ListItemType));
}

unsafeList.List.Insert(index, item);
return list;
}

/// <summary>
/// Removes the item at the specified index.
/// </summary>
/// <typeparam name="T">the base type of the covariant list.</typeparam>
/// <param name="list">The list.</param>
/// <param name="index">The index of the item to remove.</param>
/// <returns>the enumerable</returns>
public static IIndexedEnumerable<T> RemoveAt<T>(this IIndexedEnumerable<T> list, int index)
{
((IUnsafeList)list).List.RemoveAt(index);
return list;
}

#region Helper Classes and Interfaces

/// <summary>
/// Gives access to the properties of the underlying list for a CovariantList
/// </summary>
private interface IUnsafeList
{
/// <summary>
/// Gets the list.
/// </summary>
IList List { get; }

/// <summary>
/// Gets the type of the list item.
/// </summary>
/// <value>
/// The type of the list item.
/// </value>
Type ListItemType { get; }
}

/// <summary>
/// Wraps a generic list so that it can be used covariantely while still giving access to much used properties
/// </summary>
/// <typeparam name="T">The type of the underlying list.</typeparam>
private class CovariantList<T> : IIndexedEnumerable<T>, IUnsafeList
{
#region Fields

private readonly IList<T> _list;

#endregion


#region Constructors

/// <summary>
/// Initializes a new instance of the <see cref="CovariantList&lt;T&gt;"/> class.
/// </summary>
/// <param name="list">The underlying list.</param>
public CovariantList(IList<T> list)
{
// It doesn't matter if the list is null, we still want the methods to work
_list = list ?? new List<T>();
}

#endregion


#region Implementation of IEnumerable

/// <summary>
/// Returns an enumerator that iterates through the collection.
/// </summary>
/// <returns>
/// A <see cref="T:System.Collections.Generic.IEnumerator`1"/> that can be used to iterate through the collection.
/// </returns>
/// <filterpriority>1</filterpriority>
public IEnumerator<T> GetEnumerator()
{
return _list.GetEnumerator();
}

/// <summary>
/// Returns an enumerator that iterates through a collection.
/// </summary>
/// <returns>
/// An <see cref="T:System.Collections.IEnumerator"/> object that can be used to iterate through the collection.
/// </returns>
/// <filterpriority>2</filterpriority>
IEnumerator IEnumerable.GetEnumerator()
{
return GetEnumerator();
}

#endregion


#region Implementation of IIndexedEnumerable<out T>

/// <summary>
/// Gets the <see cref="T"/> at the specified index.
/// </summary>
public T this[int index]
{
get { return _list[index]; }
}

/// <summary>
/// Gets the count.
/// </summary>
public int Count
{
get { return _list.Count; }
}

#endregion


#region Implementation of IUnsafeList

/// <summary>
/// Gets the list.
/// </summary>
public IList List
{
get { return (IList)_list; }
}

/// <summary>
/// Gets the type of the list item.
/// </summary>
/// <value>
/// The type of the list item.
/// </value>
public Type ListItemType
{
get { return typeof(T); }
}

#endregion
}

#endregion
}


And here are the tests:



[TestFixture]
public class IIndexedEnumerableTests : AbstractTest
{
#region Test Classes

public abstract class Super
{
public virtual IEnumerable<IIndexedEnumerable<Super>> SuperClassLists
{
get
{
return new List<IIndexedEnumerable<Super>>();
}
}
}

public class Sub1 : Super
{
}

public class Sub2 : Super
{
public List<Sub1> Sub1s { get; set; }

public override IEnumerable<IIndexedEnumerable<Super>> SuperClassLists
{
get
{
yield return Sub1s.AsCovariant();
}
}
}

#endregion

[Test]
public void Add()
{
var sub2 = new Sub2
{
Sub1s = new List<Sub1>
{
new Sub1(),
new Sub1(),
new Sub1(),
}
};

foreach (IIndexedEnumerable<Super> superClassList in sub2.SuperClassLists)
{
superClassList.Add(new Sub1());
}

Assert.AreEqual(4, sub2.Sub1s.Count);
}

[Test]
public void Add_TypeCheck()
{
var sub2 = new Sub2
{
Sub1s = new List<Sub1>
{
new Sub1(),
new Sub1(),
new Sub1(),
}
};

foreach (IIndexedEnumerable<Super> superClassList in sub2.SuperClassLists)
{
superClassList.Add(new Sub1());
var local = superClassList;
Assert.Throws<ArgumentException>(() => local.Add(new Sub2()));
}
}

[Test]
public void Index()
{
var sub2 = new Sub2
{
Sub1s = new List<Sub1>
{
new Sub1(),
new Sub1(),
new Sub1(),
}
};

foreach (IIndexedEnumerable<Super> superClassList in sub2.SuperClassLists)
{
Assert.AreSame(sub2.Sub1s[1], superClassList[1]);
Assert.AreNotSame(sub2.Sub1s[1], superClassList[0]);
}
}

[Test]
public void Count()
{
var sub2 = new Sub2
{
Sub1s = new List<Sub1>
{
new Sub1(),
new Sub1(),
new Sub1(),
}
};

foreach (IIndexedEnumerable<Super> superClassList in sub2.SuperClassLists)
{
Assert.AreEqual(3, superClassList.Count);
}
}

[Test]
public void NullList()
{
var sub2 = new Sub2();

foreach (var superClassList in sub2.SuperClassLists)
{
// Ensure that it doesn't throw a wobbly if the Ilist was null
// You might want this, but for our purposes, we didn't want this to cause an exception.
foreach (IIndexedEnumerable<Super> superClass in superClassList)
{

}
}
}

[Test]
[Ignore("This was a spike to check that adding the type check in wasn't going to hinder performance. It didn't")]
public void TypeCheckPerformance()
{
var sub2 = new Sub2
{
Sub1s = new List<Sub1>()
};

var start = DateTime.Now;
foreach (var superClassList in sub2.SuperClassLists)
{
for (var i = 0; i < 100000; i++)
{
superClassList.Add(new Sub1());
}
}
var timeTaken = DateTime.Now - start;
Console.WriteLine("Time Taken: {0}ms", timeTaken.TotalMilliseconds);
}
}

Submit this story to DotNetKicks Shout it