Covariance and Contravariance Demystified (in C#)

hamid
7 min readMar 31, 2016

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>.

Fig.1 [a] Replace an expression that expects a subtype (as input) with another that expects its super-type. This is possible because it’s always safe to assign a child to its parent. [b] illustrates that the converse is not true.

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.

Fig.2 [a] Replace an expression that produces a super-type (as output) with another that produces its subtype. This is possible because it’s always safe to assign a child to its parent. [b] illustrates that the converse is not true.

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:

  1. passing an IEnumerable<string> to a function that expects an IEnumerable<object>
  2. 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!

--

--