Fotia Ltd

Fotia Ltd

Thursday, 18 January 2007

Covariant Generic List

posted by Stefan Delmarco

The introduction of generics in .NET 2.0 has opened up a whole new approach to class design in C#. The first, and most obvious, benefit generics provide is strongly typed containers. We no longer have to keep casting, boxing / unboxing, to / from object for ArrayList items anymore.

However, with the power of generics come a few limitations. This article explores some subtle restrictions in covariance with generics.

What is Covariance?

In mathematical terms covariance is the measure of the tendency of two things to move or vary together. In the world of object-oriented programming, covariance describes the situation in which a derived type is used where a base class was expected. For example, in .NET arrays are covariant. That is:

DerivedClass[] derivedClasses = new DerivedClass[3];
BaseClass[] baseClasses = derivedClasses;

As a worked example, let's say that we have a People class. An instance of the People class is created from a data source that requires read / write access to the class's properties. A good example of such a data source is XML serialisation. The XmlSerializer class requires read / write properties and a default constructor in order to create an object from XML. However, once this object has been created, I want the object to be immutable. I don't want any properties of the object to be changed once it has been created. The natural implementation for this requirement is to create the IPerson interface that only exposes the property getters:

// Immutable Person.
public interface IPerson
{
   string Name { get; }
}

public class Person : IPerson
{
   private string name;

   public Person()
   {}

   public string Name
   {
      get { return this.name; }
      set { this.name = value; }
   }
}

public static class PersonFactory
{
   // Factory method to create an IPerson instance.
   public static IPerson CreatePerson()
   {
      // Simulate a data source.
      Person person = new Person();
      person.Name = "Trogdor";

      return person;
   }
}

Generic Covariance Limitations

Now, let's say that we aren't creating single Person objects. Instead the data source returns an IList of Person objects. We would naturally try the following code first:

public static class PeopleFactory
{
   public static IList<IPerson> CreatePeople()
   {
      // Simulate a data source.
      List<Person> people = new List<Person>();
      // Person objects added to list.
      // ...

      return people;
   }
}

The code above does not compile. The C# compiler fails with error:

CS0030: Cannot convert type 'System.Collections.Generic.List<TestScratch.Person>' to 'System.Collections.Generic.List<TestScratch.IPerson>'

The problem we've run into here has to do with the generic type compatibility, specifically, generics are not covariant.

This is nicely summarised over on MSDN: Are Generics Covariant, Contra-Variant, or Invariant?. In a nutshell, generics cannot be covariant as it would allow illegal constructs. For example:

public class Adult : IPerson
{
   //
}

public class Child : IPerson
{
   //
}

public void WhyCoVarianceIsNotAllowed()
{
   List<Adult> adults = new List<Adult>();
   adults.Add(new Adult());
   // If the following line would be allowed...
   List<IPerson> people = adults;
   // This line would have to be legal, i.e. adding a Child to an Adult List.
   people.Add(new Child());
}

A Generic Covariant List Implementation

As dire as this seems, not all is lost. A natural extension to the immutable object requirement would be to constrain the list of immutable objects to be itself immutable. This additional restriction allows us to circumvent the generic covariance issue as the immutable list would not allow the 'illegal' situation to arise. The most natural interface to return for an immutable list is IEnumerable<T>. We therefore need to find a way of converting / casting IList<Person> to IEnumerable<IPerson>. As the C# compiler does not make any generic covariance exceptions, it will still not allow us to directly cast IList<Person> to IEnumerable<IPerson>. Instead we need to find a way to express the readonly nature of IEnumerable in a way that gets the C# compiler to accept that what we're trying to do is legal. That is what the following class accomplishes:

public class EnumerableGeneric<TClass, TInterface> : IEnumerable<TInterface>
   where TClass : TInterface
{
   private IList<TClass> list;

   public EnumerableGeneric(IList<TClass> list)
   {
      this.list = list;
   }

   public IEnumerator<TInterface> GetEnumerator()
   {
      foreach(TClass item in list)
         yield return item;
   }

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

This class pulls together pretty much all of the .NET 2.0 features. Firstly, it uses generic constraints to ensure that TClass implements TInterface (or TInterface could be a base class of TClass). Secondly, the compiler understands constraint as it allows us to use yield return to implicitly cast TClass to TInterface. This class then becomes a simple generic wrapper that can be used whenever a covariant list is required:

public static class PeopleFactory
{
   public static IEnumerable<IPerson> CreatePeople()
   {
      // Simulate a data source.
      IList<Person> people = new List<Person>();
      // Person objects added to list.

      // ...
      return new EnumerableGeneric<Person, IPerson>(people);
   }
}

Labels:

Previous Posts