Search
Close this search box.

Covariance and Contravariance in C# 4.0

C# 4.0 introduces the notion of Covariance and Contravariance of generic type parameters for interfaces and delegate types. Eric Lippert has put together a bunch of posts that goes into details of the why and how, an excellent read but not for the faint of heart. I would strongly suggest reading these posts to get a firm grounding and a better appreciation of this feature. It took me a while to get my head wrapped around this, especially since none of the VS2010 Betas were not out at the time and there was no way to try out any code. Now that I have Beta2 of VS2010 at hand I decided to try out some code samples first hand and solidify my understanding of the feature.

Suppose we have the following inheritance hierarchy:

interface IAnimal {
  void Speak();
}

class Whale : IAnimal {
  public void Speak() {
    Console.WriteLine("I'm a Whale");
  }
}

class Giraffe : IAnimal {
  public void Speak() {
    Console.WriteLine("I'm a Giraffe");
  }
}

and a processor to process animals:

interface IProcessor<T> {
  void Process(IEnumerable<T> ts);
}

class Processor<T> : IProcessor<T>
    where T : IAnimal {
  public void Process(IEnumerable<T> ts) {
    foreach (var t in ts) {
      t.Speak();
    }
  }
}

It would be natural to be able to create a Processor to process any kind of animal, but the following code does not compile today on the C# 3.0 compiler

List<Giraffe> giraffeList = new List<Giraffe> { new Giraffe() };  
IProcessor<IAnimal> proc = new Processor<IAnimal>();  
proc.Process(giraffeList); 

We have to add a silly”OfType<IAnimal>” cast to make it work:

List<Giraffe> giraffeList = new List<Giraffe> { new Giraffe() };  
IProcessor<IAnimal> proc = new Processor<IAnimal>();  
proc.Process(giraffeList.OfType<IAnimal>()); 

In C# 4.0 no such cast is needed and everything just works as expected. Why? because in .NET 4.0 IEnumerable<T> is covariant in it’s type parameter T, this is what the definition looks like

public interface IEnumerable<out T> : IEnumerable  
{  
....  
....  
}   

The “out” keyword is what makes the “T” covariant; which means that you can pass in any type which is more derived than “T” as the type argument, hence we can pass in IEnumerable<Giraffe> or IEnumerable<Whale> to the Process method which accepts an IEnumerable<IAnimal> and everything just works as expected in a type safe way.
You can define your own covariant types to allow for such conversions.
Consider the following generic class definition to create instances of a specified type

interface IFactory<out T> {
  T CreateInstance();
}

class Factory<T> : IFactory<T>
    where T : new() {
  public T CreateInstance() {
    return new T();
  }
}

IFactory<T> is covariant in T so we can treat a Factory<Giraffe> as an IFactory<IAnimal>. This allows us to treat factories of types in the same inheritance chain uniformly like so:

IFactory<IAnimal> giraffeFactory = new Factory<Giraffe>();
IFactory<IAnimal> whaleFactory = new Factory<Whale>();

List<IFactory<IAnimal>> factories =
    new List<IFactory<IAnimal>> { giraffeFactory, whaleFactory };
foreach (var factory in factories) {
  factory.CreateInstance().Speak();
}

Note that it is illegal for a covariant type parameter to occur in an input position, it can only occur in output positions such as return type of a method in the type. This restriction is key in making covariance work,allowing the type parameter in an input position would break covariance. How you might ask. Imagine if the following illegal code were legal and you could specify T in an input position.

interface IFactory<out T> {
  T CreateInstance();
  T CreateInstanceFrom(T t);
}

class Factory<T> : IFactory<T>
    where T : new()
    where T : ICloneable {
  public T CreateInstance() {
    return new T();
  }
  public T CreateInstanceFrom(T t) {
    return (T)t.Clone();
  }
}

We added a constraint to the factory class to constrain the type parameter to support ICloneable and added a new method CreateInstanceFrom which accept the instance to clone. Now what happens if we do the following:

IFactory<IAnimal> giraffeFactory = new Factory<Giraffe>();  
giraffeFactory.CreateInstanceFrom(new Whale())  

Oh my! we just passed in a Whale to clone a Giraffe, and we’d bomb a runtime.
It is precisely for this reason that a covariant type parameter can only occur in output positions(hence the keyword out) and the runtime error is now a compile time error.

But what if we wanted to have the type paramter in an input position to get the benefits variance has to offer, enter contrvariance which is the dual of covariance. In that a type argument can be safely replaced by a more derived type but only in an input position.

If T appears ONLY in an input position (such as a parameter in methods of IProcessor) then we can mark T as contravariant using the in keyword, allowing us to substitute any sub type of T.

interface IProcessor<in T>  
{  
    void Process(IEnumerable<T> ts);  
} 

This makes assignments such as the following legal.

List<Giraffe> giraffes = new List<Giraffe> { new Giraffe() };  
List<Whale> whales = new List<Whale> { new Whale() };  
IProcessor<IAnimal> animalProc = new Processor<IAnimal>();  
IProcessor<Giraffe> giraffeProcessor = animalProc;  
IProcessor<Whale> whaleProcessor = animalProc;  
giraffeProcessor.Process(giraffes);  
whaleProcessor.Process(whales);  

Finally let’s look at variance in delegate types. Consider the following delegate definition and it’s subsequent usage

public delegate void MyAction<in T>(T t);  
........  
.......  
.........  
 MyAction<IAnimal> animalAction = a => a.Speak();  

Since T is contravariant, we can treat MyAction<IAnimal> as MyAction<Giraffe> since Giraffe is more derived than IAnimal

MyAction<Giraffe> rafAction = animalAction;  

Now let’s combine both co and contravariance, consider the following delegate

public delegate T2 MyFunc<in T1,out T2>(T1 t1);  

T2 is covariant and T1 is contravariant and hence we can use T1 or any subtypes of T1 as the first generic type paramter and T2 or any of its base types as the second generic type parameter, allowing us to use assignments such as the following with full type safety

Func<IAnimal, Giraffe> func = a => a as Giraffe;  
Func<Giraffe, IAnimal> funcG = func;  

Covariance and Contravariance may sound like a mouthful and daunting to understand but at its core it’s quite simple to grasp once you understand its purpose, it enables writing generic code more naturally and allows safe type coercions. That’s it for today. Happy coding.

This article is part of the GWB Archives. Original Author: Abhijeet Patel

Related Posts