poo-covarianza-contravarianza

What is Covariance and Contravariance?

  • 4 min

The variance is the rule that decides whether a generic type can be substituted by another related through inheritance.

If you’ve been programming for a while, you’ve surely encountered strange situations where the compiler yells at you “Cannot convert List<Dog> to List<Animal>.

And you think:

“But wait… if a Dog is an Animal,

why isn’t a list of dogs a list of animals?”

The short answer is: for type safety. The long answer has to do with three key concepts: Invariance, Covariance, and Contravariance.

These terms, which sound like they’re from an advanced calculus book, simply describe how complex types (like lists, arrays, or delegates) behave when we try to substitute them with derived types.

Let’s try to understand it, without risking an aneurysm 👇.

The Base Scenario

For all the examples, we’ll use this classic hierarchy. It’s simple, but effective:

public class Animal { }
public class Dog : Animal { }
public class Cat : Animal { }

Copied!

We know, from inheritance, that this is valid:

Animal myPet = new Dog(); // ✅ Correct. A dog is an animal.

Copied!

But “the tricky part” comes when we wrap these types in “containers” or generics.

Invariance: The Default Behavior

By default, generics are Invariant. This means that List<Dog> has NO relationship with List<Animal>. They are completely different types to the compiler.

Why? Suppose the compiler let us do this:

// ❌ This does NOT compile (and thank goodness)
List<Dog> dogs = new List<Dog>();
List<Animal> animals = dogs; // Suppose this were possible

animals.Add(new Cat()); // 🙀 Disaster!

Copied!

If we could treat the list of dogs as a list of animals, we could put a Cat inside.

Since the original variable dogs still points to the same list in memory, we’d suddenly have a Cat inside a List<Dog>. When we tried to read it as a dog… Boom! Runtime error 💥.

Invariance protects us from corrupting memory by mixing incompatible types. If the generic type allows Input and Output of data (like a List<T>), it must be invariant.

Covariance: “Flows with Inheritance”

Covariance allows Wrapper<Dog> to be treated as Wrapper<Animal>, as long as that generic type is designed for it.

The assignment flow goes in the same direction as inheritance.

  • Dog -> Animal
  • IEnumerable<Dog> -> IEnumerable<Animal>

When is this safe? When the container ONLY allows us to GET data (Output), but not to put data in.

In C# (and other languages), this is usually marked in interfaces with the out keyword. The perfect example is IEnumerable<out T>.

// ✅ Covariance allowed
IEnumerable<Dog> dogs = new List<Dog>();
IEnumerable<Animal> animals = dogs;
Copied!

Since IEnumerable is “read-only” (it doesn’t have an Add method), there’s no risk of putting a Cat in. We can only iterate and get animals. And since every dog we get out is an animal, it’s a safe operation.

Practical idea: we use covariance when the generic type is used only as a return value.

Contravariance: “Flows Against Inheritance”

Here’s where our heads explode a little. Contravariance allows the opposite for input types: using something that accepts Animal where something that accepts Dog is expected.

The flow goes in the opposite direction of inheritance.

  • Animal <- Dog
  • Action<Animal> -> Action<Dog>

How is this possible? This happens when the container ONLY allows us to PUT data in (Input).

Suppose a delegate Action<T> (a function that takes a parameter and returns nothing). We have a function that knows how to “pet animals”.

void PetAnimal(Animal a) { /* ... */ }

Copied!

If I have a delegate that expects to know how to “Pet Dogs”, can I pass my generic function?

// ✅ Contravariance allowed
Action<Animal> animalExpert = PetAnimal;
Action<Dog> dogCaregiver = animalExpert;

dogCaregiver(new Dog());

Copied!

Yes! Because if the animalExpert knows how to handle any animal, then by definition they know how to handle a dog.

What we COULD NOT do is pass an Action<Dog> to someone expecting an Action<Animal>, because if we pass them a Cat, the dog caregiver wouldn’t know what to do.

In C#, this is marked with the in keyword (e.g., IComparer<in T>).

Practical idea: we use contravariance when the generic type is used only as an input argument.

Quick Summary

To avoid confusion, here’s the cheat sheet:

ConceptDirectionKeyword (C#)Primary UseExample
InvarianceNone(none)Reading and WritingList<T>
CovarianceChild -> ParentoutRead Only (Output)IEnumerable<T>, Func<T>
ContravarianceParent -> ChildinWrite Only (Input)Action<T>, IComparer<T>