04 August 2008

Generic collections and inheritance

Update - 07/2010

Covariance and contravariance support in .NET 4.0 takes care of this problem without the need for casting. Here's the relevant MSDN page: Covariance and Contravariance in Generics

I stumbled upon a small annoyance today when trying to use a generic collection of type B where a generic collection of type A is expected where B inherits from A. With arrays this works fine and the elements are implicitly cast to the base type.

Here's a snippet compiler script which illustrates the problem - list-converter.txt.

In the script I'm using BaseType and InheritingType for A and B. The script initially shows the implicit cast taking place for an array of B objects on line 19. The ArrayTest method expects an array of A but is quite happy being called with an array of B.

public static void ArrayTest(BaseType[] bar) { ... }

InheritingType[] myArray = new InheritingType[]{ it1, it2 };
ArrayTest(myArray);

If we now look at the ListTest method and try running this section of code...

public static void ListTest(List<BaseType> bar) { ... }

List<InheritingType> myList = new List<InheritingType>();
myList.Add(it1);
myList.Add(it2);
  
ListTest(myList);

...we get an error...

Argument '1': cannot convert from 
'System.Collections.Generic.List<InheritingType>' to 
'System.Collections.Generic.List<BaseType>'

We get the same result if we try an explicit cast

ListTest((List<BaseType>)myList);

Obviously allowing an implicit conversion for generics in general doesn't make a lot of sense but for lists I think it does and it's a pain to have to convert from one generic collection type to another.

Solution

Thankfully, with the help of some generics (of all things) and the ConvertAll method of List there's quite an elegant solution to this problem, we can create ourselves a nice generic list converter, here's an example:

public class ListConverter<TFrom, TTo> where TFrom : TTo
{
 public static IEnumerable<TTo> Convert(IEnumerable<TFrom> from)
 {
  return Convert(new List<TFrom>(from));
 }
 
 public static List<TTo> Convert(List<TFrom> from)
 {
  return from.ConvertAll<TTo>(new Converter<TFrom, TTo>(Convert));
 }

 public static TTo Convert(TFrom from)
 {
  return (TTo)from;
 }
}

We use two type arguments, the type we're converting from, which will be B from the example above, and the type we're converting to, A. Notice also the where TFrom : TTo which enforces that B must inherit from A.

As we need the ConvertAll method of List we have a method that takes an IEnumerable and creates a new List. We also have the method that does the main ConvertAll on the List and the delegate which is passed to ConvertAll.

This allows us to create a type converter as we code without needing to mess about e.g.

ListTest(ListConverter<InheritingType, BaseType>.Convert(myList));

4 comments:

timvw said...

I feel your pain ;) I ended up creating a similar method on my EnumerableHelper class:

public static IEnumerable<TBase> Convert<TDerived, TBase>(IEnumerable<TDerived> enumeration)
where TDerived : TBase
{
foreach (TDerived element in enumeration)
{
yield return element;
}
}

Derek Fowler said...

I had no idea you could use yield like that. That's a better solution than mine, thanks for sharing.

Unknown said...

If you're using .Net 3.5 you can use the Enumerable.Cast and Enumerable.OfType extension methods to do the same thing.

Jason McAffee said...

Agree with Sam. A much easier way is to call .Cast() and then .ToList()

i.e.
List&ltTDerived&gt derivedList = new...

public static ListTest(List&ltTBase&gt base){...}

ListTest(derivedList.Cast&ltTBase&gt().ToList&ltTBase&gt());