Contravariance and Covariance is an advanced language feature in .NET. While it isn’t ever necessary, it can add a really nice flair of finesse to a library. It is also a great example of the power of the .NET framework and how much careful thought has gone into crafting it. This article will start with a general introduction to basic polymorphism concepts (needed to understand this feature) then move on to the specifics of contravariance and covariance.
In a nutshell polymorphism is a tool of abstraction. It allows developers to be specific when needed and vague when specificity isn’t needed. This control comes from type checker enforced relationships among types. For the most part these relationships have to be explicitly defined by the developer. However, in the case of contravariance and covariance the type checker is able to infer more complex type relationships than a developer has explicitly defined.
To make this a little more clear let’s look at a simple example.
Let’s say we are working with the following three types.
We have created a base Animal type which inherits from Object and two Animal subtypes: Beaver and Dragon. In standard type polymorphism then, any subtype can be assigned to any of its supertypes. For our types that would look like this:
All these relationships were explicitly defined by the developer using the
ClassName: Parent syntax. In general, no type relationships can be inferred unless there is an explicit relationship. This is known as Nominal Typing (.NET is a Nominally Typed language).
Contravariance and Covariance, while different sides of the same coin, aren’t equally approachable. At least for me, Covariance was pretty straight forward to learn and use (in fact I bet you’ve used it without even knowing it). Contravariance on the other hand, has feels like I’m using double negatives. I lose track of what it is I’m actually saying when using it.
To look at Covariance let’s say we have the following types:
In this case Covariance would allow the following assignments:
Notice that we never explicitly defined that Sheep is a subtype of Animal
- Is Sheep of type Animal
- Is Baa of type Cry
In this case we did define both of those relationships explicitly.
Contravariance works the same way except in reverse (at least in regards to the generic types, the root type is still checked in the normal way).
Which would allow the following assignments
In this case the type checker performs the following checks:
- Is Eater of type Animal
- Is Kelp of type Food
Contravariance and Covariance are powerful and useful features when needed. They allow for exponentially more variability in type assignment without adding much complexity for the developer. Perhaps the biggest drawback is that Contravariance is very hard to make sense of. It smacks of the same problem that Little-Endian runs into: one has to think in two different directions at once. For more reading on this subject I recommend Jon Skeet1, Wikipedia2 and MSDN3.