Covariance and Contravariance in C# 4.0

Yan Cui

I help clients go faster for less using serverless technologies.

This article is brought to you by

I never fully recovered my workspace setup when I upgraded my laptop two years ago, and I still miss things today. If only I had known about Gitpod back then…

Learn more

Over the last couple of days I have read a number of blogs on the topic of covariance and contravariance, a new feature to be introduced in the upcoming C# 4.0 release. Whilst most of the blog posts were happy to provide a detailed account of what these terms mean and provide some simple examples, none really hit home how they differ from polymorphism (or assignment compatibility, the ability to assign a more specific type to a variable of a less specific type). That is, until I stumble across this excellent post on Eric Lippert’s blog which explains exactly that!

Covariance

For those of you who’s new to the topic of covariance and contravariance, here’s some code snippets to demonstrate what covariance means and the problem with it:

class Animal {}
class Dog : Animal {}
class Cat : Animal {}

void ChangeToDog(Animal[] animals)
{
    animals[0] = new Dog(); // this throws runtime error because of type mismatch
}

void Main()
{
    // this is fine, by virtue of polymorphism
    // this is also known as 'type variance' because types of the two variables 'vary'
    Animal animal = new Cat();

    // this is also fine, this is called 'covariance'
    // this works because the rules for arrays is co-ordinated with the rules for variance
    // of the array element types
    Animal[] animals = new Animal[] { new Cat() };

    // here's the problem - because the ChangeToDog method accepts an array of Animal
    // objects therefore it has to accept an array of Cat objects, and the subsequent
    // assignment is also valid to the compiler because it follows the rules of
    // variance, but clearly, you can't assign a Dog to an array of Cat objects!
    ChangeToDog(new Cat[] { new Cat() });
}

Firstly, the problem demonstrated above throws a runtime error, which is not ideal as we C# developers prefer compile time errors. But the main issue here is that this is a WTF error because it catches you completely by surprise, as you’re writing this method there is no way for you to know that the consumer of your method will call it covariantly and cause a runtime exception to be thrown for an otherwise perfectly valid assignment.

So when generics became available in C# 2.0, the designers of C# took away the ability to use type variance with generics as you can see from the code snippets below:

class Animal {}
class Dog : Animal {}
class Cat : Animal {}

void ChangeToDog<T>(List<T> animals) where T : Animal
{
    // compile time error
    animals[0] = new Dog();

    // same as above, compile time error
    animals[0] = (T) new Dog();

    // this compiles but throws a runtime type mismatch error as before, but no longer
    // a WTF error because this has 'suspicious looking code' written all over it!
    animals[0] = (T) (object) new Dog();

    // this is pretty much the safest way to do the type of casting we want
    if (animals is List<Dog>)
    {
        animals[0] = (T) (object) new Dog();
    }
}

void Main()
{
    // this no longer works!
    List<Animal> animals = new List<Cat>() { new Cat() };
}

The inheritance relationship between a Cat and an Animal is not preserved because there was no covariance support for generics in C# 3.5.

In C# 4.0, you will be able to use covariance with interfaces on types that are only used at output positions which makes it safe and prevents the WTF exception from the first example from happening.

To use covariance in your code, you need to mark a type variable with the out keyword, here’s the new look IEnumerable<T> and IEnumerator<T> in C# 4.0:

public interface IEnumerable<out T> : IEnumerable
{
    IEnumerator<T> GetEnumerator();
}

public interface IEnumerator<out T> : IEnumerator
{
    bool MoveNext();
    T Current { get; }
}

Contravariance

Conversely to covariance, contravariance reverses the inheritance relationship of T:

public class CustomEventArgs : EventArgs { }
// delegate with handler which accepts type CustomEventArgs
public EventHandler<CustomEventArgs> MyDelegate;
// handler with a less specific type EventArgs
public void MyHandler(object sender, EventArgs e) { }

void Main()
{
    // this works thanks to contravariance
    MyDelegate += MyHandler;
}

Right now, contravariance is supported in generic delegates in the handler’s event argument as I have demonstrated above, but not in the type parameter T so this bit of code doesn’t compile:

class Person {}
class Student : Person {}

// throws compile error for type mismatch because Action<T> is not contravariant in T
Action<Student> studentAction = (Person p) => { /* do something */ };

In C# 4.0, you can use contravariance on a type that is only used in input positions which allows you to cast in the reverse direction (e.g. from Animal to Dog).

The in keyword can be used to mark an input type as contravariant, the limitation is that you are only allowed it for reference conversion, so no boxing allowed!

public interface IComparer<in T>
{
    int Compare(T left, T right);
}

This means you will be able to use an IComparer<object> as though it was a IComparer<string>.

As you’d imagine, covariance and contravariance is also supported in the type parameters for generic delegates, the default Func and Action delegates have all been marked with the appropriate in or out keywords for each type parameter.

Further reading

Eric Lippert’s 11-part (so far!) blog entries on Covariance and Contravariance

Joe Albahari’s presentation on What’s New in C# 4.0

Whenever you’re ready, here are 3 ways I can help you:

  1. Production-Ready Serverless: Join 20+ AWS Heroes & Community Builders and 1000+ other students in levelling up your serverless game. This is your one-stop shop for quickly levelling up your serverless skills.
  2. I help clients launch product ideas, improve their development processes and upskill their teams. If you’d like to work together, then let’s get in touch.
  3. Join my community on Discord, ask questions, and join the discussion on all things AWS and Serverless.

2 thoughts on “Covariance and Contravariance in C# 4.0”

Leave a Comment

Your email address will not be published. Required fields are marked *