Covariance and Contravariance in C# 4.0

posted in: Language Features | 0

C# 4.0 allows to declare variance compatibility for delegates and interfaces. This means, for instance, that one can assign an IEnumerable<Cat> to an IEnumerable<Animal>. The term variance compatibility, in this context, defines the kind of assignment compatibility between two closed generic types, which exists when the parameters of those types are derived from each other (or are themselves variant to each other). In other words: Given two types T1<P1> and T2<P2>, variance defines how T1 is assignment compatible with T2 in cases where P1 is in an inheritance relationship with P2 (or is itself variant to P2). A more detailed definition can be found in the C# 4.0 language specification, § 13.1.3 Variant type parameter lists. There are three kinds of variance:

Kind of Variance Delegate Example Interface Example
Invariance delegate T Clone<T>(T t); interface IList<T>{
    T this[int index] { get; set; }
}
Covariance delegate T Func<out T>(); interface IEnumerator<out T>{
    T Current { get; }
};
Contravariance delegate void Action<in T>(T t); interface IComparer<in T>{
    int Compare(T x, T y);
}

Invariance

In the above table, Clone<T> and IList<T> are invariant in T. As a consequence, it is not possible to assign Clone<Dog> to Clone<Animal>, or vice versa. This is because T is used both as an incoming parameter and an outgoing parameter at the same time. Imagine what would happen if one could actually assign an IList<Dog> to an IList<Animal>. If we then retrieved animals[0], we would get a dog as animal. So far, so good. However, what would happen if we set animals[0] = new Cat()? What should the underlying IList<Dog> do when it gets the cat assigned? Should it perhaps throw an InvalidCastException? There is no way this can be handled without violation of the strong typing principle. Therefore, the core languages of the .Net Framework: C#, F# and VB.NET (with Option Strict on) do not allow covariance or contravariance in this situation.

IList<Animal> animals = new Collection<Animal>();
IList<Cat> cats = new Collection<Cat>();
// The following line would produce a compiler error.
// animals = cats;
// Thanks to the above compiler error, we can always be sure
// that the following line will not produce a runtime exception.
animals.Add(new Dog());

Unfortunately, ever since C# 1.0, strong typing has not been completely enforced with regards to arrays (many other languages, e.g. Java, have the same problem). The following programming error is not prevented by the compiler:

Cat[] cats = { new Cat(), new Cat() };
Animal[] animals = cats; // Compiler allows covariance, eventhough in parameters exist.
animals[0] = new Dog(); // --> ArrayTypeMismatchException at runtime!

It is sometimes argued that breaking type safety with arrays was a pragmatic decision at the time of C# 1.0, because generics did not exist yet. However, according to Don Syme, who was responsible for generics in the CLR, back then it was not even certain whether generics would ever be introduced into C#. Microsoft was reluctant, and the responsible research team was underfunded. Generics finally made it into C# 2.0 at the very last minute, under extreme time pressure.

Covariance

In the above table, Func<out T> and IEnumerator<out T> are covariant in T. As a consequence, it is possible to assign Func<Cat> to Func<Animal>. Strong typing can be enforced by the compiler, because T is only used as outgoing parameter, never as incoming parameter. To enable covariance in T, T must be explicitly annotated with the generic modifier out.

Func<Animal> createAnimal = () => new Animal();
Func<Cat> createCat = () => new Cat();
createAnimal = createCat;
var a = createAnimal(); // Gets a cat as animal.

Contravariance

In the above table, Action<in T> and IComparer<in T> are contravariant. Therefore, it is possible to assign Action<Animal> to Action<Cat> or IComparer<Animal> to IComparer<Cat>. Strong typing can be enforced by the compiler, because T is only used as incoming parameter, never as outgoing parameter. To enable contravariance in T, it must be explicitly annotated with the generic modifier in.

Action<Animal> animalAction = animal => animal.Sleep();
Actionn<Cat> catAction = cat => cat.Meow();
catAction(new Cat()); // Meow...
catAction = animalAction;
catAction(new Cat()); // Zzz...

Other Considerations

Classes and structs cannot be declared as covariant or contravariant. Even if such a syntax were allowed, the feature would be useless in most cases. For instance, it would be logically impossible to assign a Collection<Cat> to a Collection<Animal>, because Collection<T> uses T both as an incoming parameter and an outgoing parameter. Collections, even if they are immutable, somehow need a way to be initialized (which usually implies using T as incoming parameter) and a way to be read from (using T as outgoing parameter). Still, there are at least some cases where covariant classes might make sense (e.g. a Builder<out T> or Deserializer<out T>), but they're probably not worth extending the compiler.

I have just covered the basics about variance in C#, the rest can mostly be concluded by logical deduction and combination (e.g., dealing with ref and out method parameters, etc.). For further details, I recommend Eric Lippert's excellent twenty-one-part blog series on the subject.

Comments are closed.