Covariance and contravariance

You can do:

Animal animal = new Cat();

but you cannot do:

List<Animal> animals = new List<Cat>();

Why?

The problem is, that if this would be allowed, then this could happen:

List<Animal> animals = new List<Cat>();
animals.Add(new Dog());

By going through the intermediary animals list, we just put a Dog in the Cats list.

And we could make the Dog meow:

List<Cat> cats = new List<Cat>();
List<Animal> animals = cats; // the only problematic line
animals.Add(new Dog());

foreach (var cat in cats)
{
    cat.Meow();
}

For this reason, even though Cat inherits Animal and can be assigned, T<Cat> is not assignable to T<Animal>.

But for some cases, it would be really nice, I want to feed all of my pets (two cats and a dog):

List<Cat> cats = new List<Cat> { new Cat(), new Cat() };
List<Dog> dogs = new List<Dog> { new Dog() };
List<Animal> pets = cats + dogs;

foreach (var pet in pets)
{
    pet.Eat();
}

Here, I'm not hiding the dog among cats, I just want them together, no matter who they are. I would give up my ability to write to this list later, just to feed my pets!

Turns out, you can give it up, and feed everyone:

List<Cat> cats = new List<Cat> { new Cat(), new Cat() };
List<Dog> dogs = new List<Dog> { new Dog() };
IEnumerable<Animal> pets = cats.Concat<Animal>(dogs); // works!

foreach (var pet in pets)
{
    pet.Eat();
}

Here, I replaced pets list with IEnumerable, which is a kind of sequence you cannot write to. .Concat creates a new sequence, and in the definition of IEnumerable you can see:

public interface IEnumerable<out T> { }

That out in <out T> means that you can only take T's out, and cannot put anything in to break the list.

That's what covariance is.

Contravariance is the same, except it says <in T>, so you can put stuff in, but not take it out.


Developed on Github, hosted on Vercel, powered by Next.js

by Arseny Garelyshev, © 2025

production 745bf5