Monday, September 20, 2010

Covariance & Contravariance - Part 1

One of the new features of C# 4.0 is support Covariance and Contravariance. The most basic example of the concept is pretty straightforward to grasp, but after further inspection, you'll find it can be tricky. In short, the concept describes how to determine equivalence between types. There are only 3 cases to consider when determining if two types are equivalent. Given two types S and T:
  1. S is more general than T.
  2. S is more specific than T. 
  3. Neither case 1 nor 2.

You already know from experience that in case 1, you can assign an instance of type T to a variable of type S. The reason you can do this is because the compiler assumes that because S is a superclass (more general) than T, then a variable of type S can surely point to an instance of type T. This type of determination by the compiler is called Covariant

Covariance has actually been around since .NET 1.0, we just didn't realize it. Take the following example:
Employee[] employees = new Employee[10];
Person[] people = employees;

Array assignments in C# have always been assumed to be covariant. When C# 2.0 came around, they added another kind of implicit covariance. Take a look at the following:

delegate TOut GetItem<TOut>();

Programmer GetProgrammer()
    return new Programmer();

GetItem<Employee> getEmployee = GetProgrammer;

This should make sense because if I execute getEmployee, the result would still be an instance of type Employee since a Programmer is an Employee. The equivalence operation is going from specific to general, which is known as covariance.

Following from our previous example, if we take our types and instead of making them be the return type, we make them the parameter type, we get the following:
delegate void PutItem<TIn>(TIn item);

void PutProgrammer(Programmer item) { }

PutItem<Employee> putItem = PutProgrammer;

All we've done is move the types from being a return value, to a parameter value. If you were to try this, it would be illegal in C# because you could potentially pass in an instance of ProductManager into the putItem method reference; which would mean you're passing in a ProductManager instance into the PutProgrammer method which expects a Programmer. So if the PutProgrammer method were accessing a Programmer specific property such as KnownProgrammingLanguages, it would fail because that property doesn't exist on a ProductManager. In order to make this legal, we'd have to reverse the types in the assignment operation on line 5, and the type in the method on line 3:
delegate void PutItem<TIn>(TIn item);

void PutEmployee(Employee item) { }

PutItem<Programmer> putItem = PutEmployee;

Now this is valid because I can only put a Programmer instance into the putItem method, the PutEmployee method that putItem points to will be able to accept the Programer instance since a Programmer is an Employee. Notice now that the operation on line 5 is now reversed. This is an example of case 2 because the equivalence operation on line 5 is now going from general to specific. This is known as contravariance, which (surprise!) is the reverse of covariance. 

Naturally, methods are covariant in their return types, and contravariant in their parameter types. This is not always the case, but is an important observation.

In Covariance & Contravariance - Part 2, I'll go into how this seemingly meaningless knowledge can be applied to C# 4.0.

No comments:

Post a Comment