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?
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, or
bivariant.
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.