A dog is an animal. Is a list of Dogs a list of Animals?
It depends on how you use it, and what need from it.
Generic Types
Generics are a type system feature which allows instantiating a generic type with type arguments.
A common example is List<T>,
which stores instances of its type argument T as a sequential collection.
List<T> is mutable.
We can add items using its Add method.
numbers.Add(4);number=numbers[3];// 4
Some generic classes are immutable.
IEnumerable<T>
is a generic sequence of T, which provides a read-only view of a List<T> whose entries come one at a time.
IEnumerable<T>sequence=numbers;
Assignment Compatibility
In an inheritance relationship there are (at least) two classes involved:
the base class and the derived class.
A derived class instance is always substitutable for a base class instance,
so the derived class is said to be a subtype of the base class supertype.
In this example Dog is a subtype of Animal.
Any time an Animal is needed, a Dog may be used instead.
That is to say, a Dog is substitutable for an Animal.
What about a collection of animals?
Is a list of Dogs substitutable for a list of Animals?
Covariance
A generic type S is said to be covariant if, given type arguments T and U
where T is a subtype of U, S<T> is a subtype of S<U>.
For example, IEnumerable<Dog> is a subtype of IEnumerable<Animal>
because Dog is a subtype of Animal and IEnumerable<T> is covariant.
This makes sense. Since Dog is always substitutable for Animal,
a function observing a sequence of animals won’t mind if some or all of them are dogs.
So far so good. Let’s take our dogList from earlier and add more dogs to it.
AddDogs(dogList)// Error!
Wait, what? Why can’t I add dogs to a list of dogs?
The problem is that AddDogs doesn’t work with List<Dog>–
it works with List<Animal>.
List<T> is not covariant, it is invariant.
List<Dog> is not a subtype of List<Animal> in the way that
IEnumerable<Dog> is a subtype of IEnumerable<Animal>.
AddDogs expects a List<Animal>, for which List<Dog> is not substitutable.
List<Dog> and List<Animal> are both subtypes of IEnumerable<Animal>,
so either can be used wherever an IEnumerable<Animal> is required.
However List<Dog> cannot be used wherever a List<Animal> is required.
But why does the subtype hierarchy look this way?
Why can’tList<Dog> be a subtype of List<Animal> directly,
instead of going through this IEnumerable path?
Why isn’t List<T>covariant?
It comes down to mutability.
Since List<T> is mutable, we can add instances of T to it.
Remember the rule about assignment compatibility:
we can substitute any instance of a subtype for an instance of the supertype.
Since Dog and Cat are both subtypes of Animal,
we can add either to a List<Animal>.
See the problem? animals is just a reference to dogs,
and we tried to add a Cat to it.
List<Dog> can’t store cats, it can only store dogs.
Therefore a mutable collection can never be covariant1.
The C# compiler will not allow dogs to be assigned
as a value to a variable of type List<Animal>.
This problem goes away if the collection is immutable because it is now impossible
to add a cat to a list of dogs— there is no Add method on IEnumerable<T>.
Contravariance
This idea of covariance doesn’t just apply to container types;
it is relevant to any generic type with type parameters which have a subtype/supertype relationship.
Consider Action<T>,
which represents a callable function with a single argument of type T and no return value.
Action<Animal>speak=SayNoise;
Is Action<T> covariant? Can we substitute Action<Dog> for Action<Animal>?
voidDogSpeak(Dogdog){Console.WriteLine($"The dog says {dog.SayNoise()}");}Action<Dog>speakDog=DogSpeak;Action<Animal>speakAnimal=speakDog;Animalcat=Cat();speakAnimal(cat);// !!!
This won’t work. DogSpeak requires a Dog, and by substituting Action<Dog>
for Action<Animal> we can now provide a Cat instead.
The opposite could work though: a function expecting Animal would be happy to receive a Dog.
The Action<Dog> interface forces callers to provide a subset of possible Animals
(i.e., Dogs), but can’t add anything that a function expecting an Animal
would be unprepared to handle.
Action<Animal> is substitutable for Action<Dog>!
They have an assignment compatibility relationship opposite that Animal and Dog do.
This reversal of subtype/supertype relationship in the generic type means that Action<T>
is contravariant.
This property of co- and contravariance isn’t something the compiler can determine automatically.
It is opt-in,
by declaring the type arguments as out (covariant) or in (contravariant).
The keywords provide a hint as to how the type argument may be used.
Covariant type arguments can only be outputs (i.e., method return types),
and contravariant type arguments can only be inputs (i.e., method parameter types).
We can conceptualize the subtype relationships by imagining
that the supertype “contains” the subtype.
It doesn’t really, but we can think of the covariance and contravariance relationships as “wrapping” the inner type.
Here we see that the innermost List<Dog> can “forward” its contents to the enclosing IEnumerable<Dog>,
which can then forward them to the enclosing IEnumerable<Animal>, because IEnumerable is covariant.
The output of IEnumerable<Dog> is a Dog,
which we can use in an Action<Dog> directly.
The output of IEnumerable<Animal> is, in this case, a Dog under the hood, but the IEnumerable<Animal> interface hides this and presents the Animal supertype instead.
This cannot be used in an Action<Dog>, but can be used in Action<Animal>. However because in this case our Action<Dog> is itself a “wrapper” around an Action<Animal>,
Dogs passed to it are substituted for Animals in the enclosed Action<Animal>.
Notice that arrows only ever leaveIEnumerable blocks
and only ever enterAction blocks.
If it were possible to pass an Animalinto an IEnumerable<Animal>,
then it couldn’t be covariant because an underlying object expecting Dogs could receive arbitrary Animals—
(just like a List<Dog> could if used as a List<Animal>).
Similarly, if it were possible to return an Animal from an Action<Animal>,
then it couldn’t be contravariant because it could return arbitrary Animals to callers expecting Dogs.
Summary
Covariancepreserves assignment compatibility: IEnumerable<Dog> is usable in place of IEnumerable<Animal>, just as Dog is usable in place of Animal.
Contravariancereverses assignment compatibility: Action<Animal> is usable in place of Action<Dog>.
Invariancebreaks assignment compatibility: List<Dog>cannot be used in place of List<Animal>.
The problem is actually due to “input” mutability.
A remove-only list is mutable
but can still be covariant because there is never
an opportunity to insert an invalid item type into the collection.
I address this in more detail later.
RemoveList<Animal> Subtype Hierarchy
(Source)(Download)↩︎