LINQ Farm: Covariance and Contravariance in C# 4.0

This post covers the upcoming C# 4.0 support for covariance and contravariance when working with delegates and interfaces. Eric Lippert’s series of posts on this subject are definitely the definitive reference at this time. I’m writing this overview of the subject simply as an appendix to his explanation, and as quick reference for folks who want to get up to speed on this technology. Please remember that this post covers pre-beta technology as defined in the October 2008 CTP of Visual Studio 2010.

The support for covariance and contravariance in the next version of the C# language will ensure that delegates and interfaces will behave as expected when you are working with generic types. In Visual Studio 2008, there are rare occasions when developers might expect a delegate or interface to behave one way, only to find that it does not conform to their expectations. In Visual Studio 2010 delegates will behave as expected. Other C# types support have always automatically supported covariance and contravariance. C# 4.0 will simply ensure that generic delegates and interfaces follow suit.

Consider this simple class hierarchy:

class Animal{ }
class Cat: Animal{ }

Here we have a class called Animal, and a simple descendant of it called Cat. Suppose you declare a delegate declaration that defines a method that returns an arbitrary type:

 delegate T Func1<T>();

 An instance of this delegate could be defined to return a Cat:
 Func1<Cat> cat = () => new Cat();

The delegate declaration shown here defines a delegate that returns a type T. The second line of code initializes T to be of type Cat and assigns this delegate to a simple lambda that creates an instance of a cat and returns it. For those of you not yet comfortable with lambda syntax, I’ll add that the lambda shown here is simply a shorthand way of writing a method that looks like this:

 public Cat MyFunc()
{
    return new Cat();
}

So one could also have written the cat delegate like this:

Func1<Cat> cat = MyFunc;

Given either of these declarations, our intuition tells us that we could assign a cat to a delegate that returns an Animal:

 Func1<Animal> animal = cat;

This code looks like it should succeed because a Cat is type of Animal, and one should always be able to assign a smaller type of object, such as a Cat, to a larger type of object, such as a Animal.  We think of the type Animal as being large, since a creature such as a Cat, Dog, Whale or Bird would be a type of Animal, and hence compatible with it. In other words, a big type like an Animal is assignment compatible with lots of smaller types, such as a Cat, Dog or Whale. We think of the type Cat as being smaller than an Animal, since only other cats can be assigned to it. It is not assignment compatible with as wide a range of types as an Animal, and hence we think of it as being smaller.

In fact, the assignment shown above does not work in Visual Studio 2008. The new feature being added in Visual Studio 2010 ensures that this assignment will work if you make one minor change to the declaration for your delegate. In particular, you need to use the keyword out in your type parameter:

 delegate T Func1<out T>();

Now the assignment will succeed, and the code will run as expected, as shown in Listing 1. This type of assignment is called covariance.

Listing 1: Examples of covariance and contravariance.

 using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;

namespace SimpleVariance
{
    class Animal { }
    class Cat: Animal { }

    class Program
    {
        // To understand what the new CoVariance and ContraVariance code does for you
        // Try deleting or adding the words out and in from the following 2 lines of code:
        delegate T Func1<out T>();
        delegate void Action1<in T>(T a);

        static void Main(string[] args)
        {
           // Covariance
            Func1<Cat> cat = () => new Cat();
            Func1<Animal> animal = cat;

            // Contravariance
            Action1<Animal> act1 = (ani) => { Console.WriteLine(ani); };
            Action1<Cat> cat1 = act1;
        }        
    }
}

Looking at Listing 1, you will notices that there is a second example included in it. This second example illustrates contravariance. Most developers, including myself, find contravariance more difficult to understand than covariance. It is, however, a similar concept.

The second example starts out by including a delegate declaration. This time the declaration defines a method that takes a parameter, rather than returning a value:

delegate void Action1<in T>(T a);

Notice also that we use the keyword in, rather than the keyword out. That is because we are passing a parameter in, rather than returning a result “back out” of a function.

Next we define a delegate that takes an animal as a parameter:

Action1<Animal> act1 = (ani) => { Console.WriteLine(ani); };

In the contravariant example shown in Listing 1, the assignment of act1 to the delegate cat1 would fail if we did not include the keyword in when declaring the Action1 delegate type. Here is the assignment:

Action1<Cat> cat1 = act1;

This would fail in C# 3.0 no matter what you did. It will work in C# 4.0 so long as you use the keyword in when declaring the delegate type Action1. If you omitted the keyword, then it would fail. For instance, the following declaration of Action1 would cause the assigned to fail because the keyword in is omitted:

delegate void Action1<T>(T a);

Contrast this declaration with the one in Listing 1.

In the contravariant example we use a lambda to define a method that is equivalent to a standard method that looks like this:

public static void Contra(Animal ani)

{

      Console.WriteLine(ani);

}

I’m using lambdas in Listing 1 simply to keep the code short, not because lambdas are connected to covariance and contravariance. This code would still illustrate covariance and contravariance whether or not I used lambdas. What is important here is not the presence of lambdas, but the presence of a generic delegate.

Finally, lets take one moment to make sure you understand why the assignment of act1 to cat1 works. The peculiar thing about contravariance is that it appears that we are assigning a larger type to a smaller type. In other words, it appears that we are trying to fit a big thing like an Animal, into a small type like a Cat. This is not what we are doing. The point here is that act1 will work with any animal, and a Cat is animal, therefore you can safely make the assignment. In other words, cat1 can only be passed Cats, and all cats are animals, therefore anything you can assign to cat1 could also be safely passed to act1. This is why the assignment should work, and does in fact work if you use the keyword in.

As I say, contravariance is a bit confusing. If you find the subject a bit much, then you are joining a happy throng of other developers who struggle with the subject. My suggestion is just to relax, think about the example I’ve shown here for a bit, and you will probably have an “ah-ha” moment either in the next few moments, or sometime in the hopefully very lengthy remainder of your life.

Almost everyone who has written or talked about this subject, from Eric Lippert to Anders himself, points out that this technology is not so much an innovation as a means of bringing C# in line with developers expectations. Adding support for covariance and contravariance to generic interfaces and delegates simply ensures that the language behaves as many would intuitively expect it to behave. All you have to do is remember to add the the keywords in or out when you are making an assignment that involves delegates or interfaces, and the language isn’t behaving as you would expect it to behave.

I apologize for focusing only on delegates in this post. Hopefully I’ll find the time to come back and illustrate the same subject using interfaces. I should add that all the code shown here is written against a very shaky pre-beta version of the C# 4.0, and it is always possible, though that not necessarily likely, that this technology will change before it ships.

kick it on DotNetKicks.com