If you have ever felt frustrated trying to understand what covariance and contravariance really mean then this post is for you. Like many others, I banged my head against the wall trying to wrap my head around these two concepts for quite a long time. This article is an attempt to explain them in simple terms.
What is this about?
Covariance and contravariance are a consequence of one of the simplest rules of thumb you learn when you are introduced to Object-Oriented Programming:
If Cat is subtype of Animal, then an expression of type Cat can be used wherever an expression of type Animal is used.
Source: http://en.wikipedia.org/wiki/Covariance_and_contravariance_(computer_science)
This rule of thumb can be thought of as a rough definition of an important concept called Subtyping. For instance, as a junior programmer, you might have read some strong warnings about downcasting, possibly accompanied by a code snippet like the one below:
class Animal
{
}class Cat : Animal
{
}class Program
{
static void Main(string[] args)
{
Animal a = new Cat(); // OK: Child to parent Cat c = new Animal(); // Compiler error: Cannot
// implicitly convert Animal to Cat
}
}
This is just a fancy way of saying: you can treat an instance of a derived class as an instance of its parent class. Why? Well, because a derived class has everything its parent does. You may be thinking to yourself: yeah, but what does such a simple principle have to do with covariance and contravariance? This is what we explore next.
How are these concepts related?
First of all, let’s make our Animal and Cat types a little more interesting by adding a couple of properties to them:
class Animal
{
string ScientificName { get; set; }
}class Cat : Animal
{
string Breed { get; set; }
}
In this example, we know from our early Object-Oriented Programming days that an object of type Cat can replace an object of type Animal because Cat is a subtype of Animal. Covariance and contravariance come into play whenever we start thinking about other types that “depend on” types like Animal and Cat. Let’s see some examples.
Action<Animal> and Action<Cat>
Action<T> is a generic type in the .NET class library, one that is intended to represent a function that expects a single parameter of type T and returns nothing (void).
When types like Action<T> are used with classes that have inheritance relationships like Animal and Cat, we run into new situations that raise some interesting questions. For instance, knowing that Cat is a subtype of Animal, should it be possible to substitute an Action that expects a Cat with another that expects an Animal? Namely, is it possible to use Action<Animal> in an expression that expects Action<Cat>?
//
// An example of an Action<Animal> that
// prints Animal.ScientificName to command line.
//
Action<Animal> animalAction =
(a) => Console.WriteLine(a.ScientificName);Action<Cat> catAction = animalAction; // Is this valid?
You may be tempted to answer: “No way! How can we substitute an Action that expects a Cat with another that expects an object of a different type?” This substitution is actually possible in this case because Cat is a sub-type of Animal. If you don’t understand why, let’s think about the consequences:
- catAction is a function that expects a Cat
- If the code snippet above is allowed, invoking catAction with a Cat will invoke animalAction passing that Cat to it, i.e.
Cat c = new Cat { ScientificName = "Felis catus", Breed = "Asian" };catAction(c); // Invokes: animalAction(c);
Is it wrong to call animalAction with a Cat object? Definitely not. It’s perfectly valid to replace an Animal with a Cat. This is exactly our good old Object-Oriented Programming rule of thumb we have been talking about all along. A direct consequence of being able to replace Animal objects with Cat objects is that we can also substitute an Action<Cat> with an Action<Animal>.
It is important to observe that the reverse substitution is not valid. Here’s a code snippet that explains why.
//
// An example of an Action<Cat> that
// prints Cat.Breed to command line.
//
Action<Cat> catAction =
(c) => Console.WriteLine(c.Breed);Action<Animal> animalAction = catAction; // Is this valid? No!//
// Worse yet, if the above snippet were valid, it would have been
// possible to invoke an Action<Cat> against an Animal!
//
Animal a = new Animal { ScientificName = "Canis lupus familiaris" };animalAction(a); // Effectively invokes catAction(a), but object a
// does not have a "Breed" property!
The key takeaway is that inheritance relationships between types have direct consequences on the exchangeability of other types that depend on them.
Func<Animal> and Func<Cat>
Func<T> is another .NET type that is intended to represent a function that expects no parameters and returns an object of type T.
Here is a simple comparison of Action<T> and Func<T>:
- Both are generic .NET types
- Both are used to refer to functions
- The only difference is that Action<T> represents a function that expects an object of type T as parameter, whereas Func<T> represents a function that returns an object of type T.
//
// An example of Action<string>
//
void Log(string s)
{
// Do something useful with s.
}//
// An example of Func<string>
//
string GetUserName()
{
// return some username.
}Action<string> logAction = Log;
Func<string> getUserNameFunc = GetUserName;
The question we should be asking ourselves now is: can we replace a Func<Cat> with a Func<Animal> like we did with Action<T>?
//
// An example of an Func<Animal> that returns some Animal object.
//
Func<Animal> animalFunc =
() => new Animal { ScientificName = "Canis lupus familiaris" };Func<Cat> catFunc = animalFunc; // Is this valid?
To answer this question, let’s study the consequences:
- catFunc is a function that returns a Cat
- animalFunc is a function that returns an Animal
- If the code snippet above is allowed, catFunc will return an Animal to the caller of catFunc, which is expecting a Cat.
Cat c = catFunc(); // Calls animalFunc() returning an Animal.
Would it be OK to assign an Animal to a Cat? That’s exactly what our Object-Oriented Programming tutors used to warn us against: you can’t assign a parent to a child.
Unlike Action<T>, it is not valid to substitute a Func<Cat> with a Func<Animal>. Is the reverse substitution possible though? Can we replace Func<Animal> with Func<Cat>?
//
// An example of an Func<Cat> that returns some Cat object.
//
Func<Cat> catFunc =
() => new Cat { ScientificName = "Felis catus",
Breed = "Siaemese" };Func<Animal> animalFunc = catFunc; // Is this valid?
Let’s study the consequences:
- animalFunc is a function that returns an Animal
- catFunc is a function that returns a Cat
- If the code above is allowed, animalFunc will invoke catFunc, thus returning a Cat to the caller of animalFunc, which is expecting an Animal. But that’s alright because a Cat is definitely an Animal.
To sum up, a direct consequence of the inheritance relationship between Cat and Animal is that we can substitute a Func<Animal> with a Func<Cat>.
Why are Action<T> and Func<T> different?
You may be wondering why the exchangeability of Animal and Cat objects was reversed between Action<T> and Func<T>, i.e.
Action<Cat> ⟸ Action<Animal>
Func<Animal> ⟸ Func<Cat>
The reason is pretty simple: Action<T> expects an object of type T as input, whereas Func<T> returns an object of type T as output.
Figure 1 illustrates this idea very simply: an expression expecting a subtype as input can be substituted with another that expects its super-type. This is similar to our first example where it was possible to replace an Action<Cat> with an Action<Animal>.
Figure 2 illustrates the second example: an expression producing a super-type as output can be substituted with another that produces its subtype. This explains why we were able to replace Func<Animal> with Func<Cat> but not the other way around.
Direction matters
As it turns out, the exchangeability of super and sub-types depends on whether these types are used as input or output.
In our first example, it was possible to replace Action<Cat> with Action<Animal> because the object involved was an input parameter to Action<T>. To indicate that Action<sub-type> can be replaced with Action<super-type>, the authors of Action<T> annotate T with the special keyword in:
//
// Copied from:
// https://msdn.microsoft.com/en-us/library/018hxwa8.aspx
//
public delegate void Action<in T>(
T obj
)
This useful substitutability is called contravariance.
In our second example, Func<Animal> was replaceable with Func<Cat> because the object involved was an output of Func<T>. To indicate that Func<super-type> can be replaced with Func<sub-type>, the authors of Func<T> annotate T with the special keyword out:
//
// Copied from:
// https://msdn.microsoft.com/en-us/library/bb534960.aspx
//
public delegate TResult Func<out TResult>()
And, yes, this is covariance indeed.
It is important to realize how useful covariance and contravariance are in practice. Thanks to these two concepts, it is possible to do things like:
- passing an IEnumerable<string> to a function that expects an IEnumerable<object>
- specifying an IComparer<Shape> where an IComparer<Circle> is expected (where Circle is a subtype of Shape)
I hope this article helped you understand covariance and contravariance a little better. It is OK if they still feel a little confusing though. Such concepts often take some time to sink in. Just keep looking at examples and don’t forget:
- These concepts are only relevant for types that “depend-on” other types, e.g. generics.
- They are a direct consequence of Subtyping; an old rule of thumb all of us know about Object-Oriented Programming.
- Direction matters (input vs. output)
Happy hacking!