107. Variance#

Having learned about subtype polymorphism we are now accustomed to the idea that a more specific type (such as a Cat) can be used in place of a more general type (such as an Animal). We think of this as Cat being a subtype of Animal and of Animal being substitutable with Cat. Simple. But, what about generic types and what about delegates?

Key point

Variance is fundamentally about substitution. When can we use a type that uses a type in place of another type that uses a type?


Fig. 107.1 A subtype can be thought of as a subset. This means that it must be possible to use the subtype anywhere where the supertype is expected. Variance deals with the question of what this means in the context of types that use instances of other types.#

Say, for example, that we have a variable of type IList<Fruit>. Can we then assign an instance of the type IList<Apple> to that variable? If apples are fruits then a list of apples should be a list of fruit, right? Actually, in most languages, the answer to this question is: no.

// T in IList<T> is invariant, so this DOES NOT compile:
IList<Apple> apples = null;
IList<Fruit> fruits = apples;
(3,23): error CS0266: Cannot implicitly convert type 'System.Collections.Generic.IList<Apple>' to 'System.Collections.Generic.IList<Fruit>'. An explicit conversion exists (are you missing a cast?)

However, if we have a variable of type IEnumerable<Fruit> we can assign it an object of type IEnumerable<Apple>.

// T in IEnumerable<T> is covariant, so this does compile:
IEnumerable<Apple> apples = null;
IEnumerable<Fruit> fruits = apples;

Conversely, if we have a variable of type Action<Apple> we can assign it an object of type Action<Fruit>.

// T in Action<T> is contravariant, so this does compile:
Action<Fruit> fruitAction = null;
Action<Apple> appleAction = fruitAction;

To understand why this is, we must understand the topic of variance. Let’s use generics to enumerate the options. If a generic type I is parameterized over the type T, we say I<T>. The question of variance deals with whether T in I<T> is:

Invariant means that it’s neither covariant or contravariant, and bivariant means that it’s both covariant and contravariant at the same time. When we know which of the above apply to T in I we can determine whether a type I<A> is a subtype of another type I<B> if A is a subtype of B.

We will explore covariance, contravariance, and invariance in the coming chapters. Later we will use this understanding to explore the Liskov substitution principle which tells us how to use inheritance safely. Then we will discuss how variance in .NET applies to classes, delegates, generic delegates, and interfaces.