Generic variance in C#, part 1 – Invariance

csharp_genericsC#’s generic types are, by default, invariant. Special cases of covariant and contravariant can be defined though. So what does that previous gobbledygook even mean? Hopefully this three-part article will help explain these three terms, and why they even exist in the first place.

The second part of this series, looks at what is probably the least well understood of the three terms: contravariance. If you feel you understand the other two terms, feel free to jump straight to that part therefore. The third, and last, part, looks at covariance and finishes with a summary of the difference between the three. But first, we start at the beginning: invariance.

As is traditional with such explanations of coding features, let’s start with some code:

As it stands, the code won’t compile. Feel free to paste it into Visual Studio or LINQPad and try it for yourself. It results in the error, CS0029 Cannot implicitly convert type 'System.Collections.Generic.List<Elephant>' to 'System.Collections.Generic.List<IAnimal>'. This rather begs the question, why? Since Elephant implements IAnimal, then the former is a subtype of the latter, ie I can assign as Elephant to a variable of type IAnimal. So why can’t I assign a list of Elephant to a variable of type list of IAnimal? The simple answer is because List<T> is invariant. An invariant generic class doesn’t allow casting between SomeClass<T> and SomeClass<DaughterOfT>. And that, in a nutshell, is what is meant by generic invariance: casting between SomeClass<T> and SomeClass<DaughterOfT> is not allowed.

So we’ve covered the questions, “why do I get the error?” (because List<T> is invariant), and “what does ‘invariant’ mean?” (it means you can’t cast between List<T> and List<SonOfT>). That still leaves the elephant-in-the-room question, “why is List<T> invariant in the first place?” The natural feeling when asking this question is to somehow feel the CLR, or C# language teams were having an off day when they added generics and somehow messed up and accidentally introduced invariance. The truth is much more mundane (and thankfully shows the team were actually wide awake the day they introduced generics!). Once more, to help answer the question (“why is List<T> invariant in the first place?”), some more code will help:

The line zoo.Add(new Mouse()); will compile just fine as it’s perfectly acceptable to add a Mouse, which implements IAnimal to List<IAnimal>. But if the line zoo = herdOfElephants; were also allowed to compile, we’d actually be adding a mouse to a herd of elephants. In real life, such an occurrence would – if the stories are to be believed – result in the elephants huddling together in a corner, or – more likely – the poor mouse would end up pancake-shaped all too quickly. In programming terms, we’d end up with the slightly more mundane, but potentially just as serious, runtime exception when the zoo.Add(new Mouse()); line was executed.

Rather than allow runtime exceptions, C#’s designers instead took the decision to treat generic types as invariant and thus catch those errors at compile-time. And that’s where the story would end, if it weren’t for covariance and contravariance. These two cases cover situations where interfaces (and this only applies to interfaces; classes, even abstract ones are always defined as invariance) commit to either generic values only being passed in, or only being passed out, respectively. In those situations, specific types of casting can be safely allowed. Contravariance is perhaps the least understood of these two concepts and so will be the topic of the next part of this series.

Posted in C#