Covariance and Contravariance in C# Generics — A Clear, Practical Guide
Most C# developers bump into variance only when they see a puzzling compiler error:
A sample error message when trying to mix generic types where it’s not allowed
or:
Another such example
The words “covariance” and “contravariance” sound abstract, but the idea is straightforward: It’s about how type relationships behave when you use them with generic interfaces and delegates. By the end of this guide, you’ll understand:
What these words mean in plain English
Why the rules exist
How to use them in your own code
A Quick Refresher: Inheritance and Generics
If you have a simple class hierarchy:
A common example class hierarchy based on animals
You know you can assign a derived type to a base reference:
Assigning a derived type to a base type
But with generics, this fails:
Compilation error resulting from misusing an invariant interface
Even though every Dog is an Animal, a List<Dog>
is not automatically a List<Animal>
. This surprises many beginners and is the problem that variance solves.
Why the Default Is Invariant
List<T>
lets you both add and retrieve items of type T
. If the compiler allowed a List<Dog>
to be treated as a List<Animal>
, you could do:
Oops- a Cat would have been placed in a collection of Dogs
Now the dogs
list contains a Cat
— type safety is broken. Because of situations like this, most generic types are invariant: you can’t substitute one generic argument for another in an inheritance chain.
Variance: When Substitution Is Safe
Variance tells the compiler: “In this particular generic interface or delegate, it’s safe to substitute one related type for another.”
There are two directions:
Comparison of Covariance vs Contravariance
The “variance” keywords are attached to the generic type parameter in the interface or delegate definition — not at the usage site.
First Intuition: Producers vs. Consumers
A simple way to remember:
A producer gives you things of type
T
→ values flow out →out
→ covariantA consumer takes in things of type
T
→ values flow in →in
→ contravariant
If a type both produces and consumes T
, it must be invariant.
Covariance in Practice: IEnumerable<out T>
Imagine a producer that only hands you objects but never accepts them:
Notice the mixing of generic types now that wasn’t allowed before
This works because IEnumerable<T>
is defined as:
Notice the “out” keyword with IEnumerable’s generic parameter T
You only read from it. A sequence of Dogs can be treated as a sequence of Animals — nothing can insert a Cat into it through the IEnumerable
interface.
Contravariance in Practice: IComparer<in T>
Now consider a consumer — something that only takes in values of type T
:
Now an example of Contravariance
IComparer<T>
is defined as:
Notice the “in” keyword with the generic type T in IComparer
The comparer only consumes values of type T
(parameters of Compare
). A general comparer for Animals can be used in any context that needs to compare Dogs — it already knows how to handle them.
The Part That Feels “Backwards”
It’s common to think: “A Dog is an Animal, so shouldn’t a consumer of Dogs work where a consumer of Animals is expected?”
The mental flip is that you’re not substituting the objects themselves; you’re substituting the ability to accept objects.
A consumer of Animals can handle any animal you pass in, including dogs.
A consumer of Dogs can handle only dogs — so it can’t be used in a context that may pass in cats as well.
A Real-World Analogy
Imagine trainers:
GeneralAnimalTrainer (
IConsumer<Animal>
) can train any kind of animal.DogTrainer (
IConsumer<Dog>
) can train only dogs.
If a program needs an IConsumer<Dog>
because it only ever hands out dogs, you can give it the general animal trainer — they’re capable of training dogs too. ✅
But if a program needs an IConsumer<Animal>
because it will hand out dogs, cats, and birds, you cannot give it a dog-only trainer — it won’t know what to do with the cats. ❌
A Table for Clarity
Example showing how type substitutions work
Seeing the Compiler Error
Here’s how it looks in code:
Code example highlighting the error
Compiler message: Cannot implicitly convert type ‘IConsumer<Dog>’ to ‘IConsumer<Animal>’. This is the compiler protecting you from a type-safety bug.
Variance in Delegates
Delegates can also be variant. Covariance in return type:
With Func, the last generic type is the return type. Since only one type is present here, this would be a method that has no parameters and returns a Dog/Animal.
Contravariance in parameter type:
Actions return void so the first and only generic type corresponds to a method parameter
Defining Your Own Variant Interfaces
You add the keywords in the definition:
Notice the out and in keywords
Now you can do:
Covariance and contravariance
Best Practices
Use
out
for producers (output-only).Use
in
for consumers (input-only).If your generic type both accepts and returns
T
, leave it invariant.Remember: variance only applies to interfaces and delegates, not to classes.
Key Takeaways
Variance is about safe substitutions of generic types across inheritance boundaries.
out
(covariance) applies to producers: you can substitute a more derived type for a base type.in
(contravariance) applies to consumers: you can substitute a more general type for a more specific type.If you get a compiler error about type conversion with generics, ask yourself:
Is the generic type a producer, a consumer, or both?
Wrapping Up
Covariance and contravariance aren’t mystical — they simply capture the direction in which type arguments can be safely substituted.
Once you internalize:
Producer → out → covariant
Consumer → in → contravariant
…the compiler’s rules start to feel natural and you can design flexible, type-safe APIs.