Covariance and Contravariance
Vaibhav • September 11, 2025
In the previous article, we explored how generic interfaces allow us to define flexible contracts that work across types. Now we’re ready to tackle a subtle but powerful concept in generic programming: covariance and contravariance. These terms may sound intimidating, but they describe how type relationships behave when generics are involved-especially in interfaces and delegates. Understanding them will help you write safer, more flexible code that plays well with inheritance and polymorphism.
What Are Covariance and Contravariance?
In plain terms, covariance allows a generic type to be substituted with a more derived type,
while contravariance allows substitution with a more base type. These concepts are only
applicable in specific contexts-namely, when working with interfaces
and delegates
.
Let’s start with a simple example. Suppose you have a class hierarchy:
class Animal { }
class Dog : Animal { }
You can assign a Dog
to an Animal
variable
because Dog
is a subtype of Animal
. That’s
basic inheritance. But what happens when you introduce generics?
List<Dog> dogs = new List<Dog>();
List<Animal> animals = dogs; // ❌ Compile error
Even though Dog
is an Animal
, List<Dog>
is not a List<Animal>
. This is where covariance and contravariance come in-to allow
safe type substitution in specific scenarios.
Covariance Explained
Covariance applies when a generic type is used in an output position-for example, as a return
value. It allows you to use a more derived type than originally specified. You declare covariance using the
out
keyword.
public interface IProducer<out T>
{
T Produce();
}
Here, T
is covariant. That means you can treat an IProducer<Dog>
as an IProducer<Animal>
-because the method only returns T
, never consumes it.
IProducer<Dog> dogProducer = new DogFactory();
IProducer<Animal> animalProducer = dogProducer; // ✅ Allowed due to covariance
This is safe because the consumer of animalProducer
only expects an Animal
, and Dog
is a valid subtype.
Covariance only works when the type parameter is used for output-like return values or read-only properties. You cannot use it in method parameters.
Contravariance Explained
Contravariance applies when a generic type is used in an input position-for example, as a
method parameter. It allows you to use a more base type than originally specified. You declare contravariance
using the in
keyword.
public interface IConsumer<in T>
{
void Consume(T item);
}
Here, T
is contravariant. That means you can treat an IConsumer<Animal>
as an IConsumer<Dog>
-because the method only accepts T
, never returns it.
IConsumer<Animal> animalConsumer = new AnimalLogger();
IConsumer<Dog> dogConsumer = animalConsumer; // ✅ Allowed due to contravariance
This is safe because the consumer expects a Dog
, and AnimalLogger
can handle any Animal
.
Contravariance only works when the type parameter is used for input-like method parameters. You cannot use it in return values.
Variance in Delegates
Variance also applies to delegates. This is especially useful when working with events, callbacks, and LINQ expressions. Let’s look at covariance first:
public delegate Animal AnimalFactory();
public delegate Dog DogFactory();
AnimalFactory factory = new DogFactory(); // ✅ Covariance
The delegate DogFactory
returns a Dog
, which
is a subtype of Animal
. So it can be assigned to AnimalFactory
.
Now contravariance:
public delegate void AnimalHandler(Animal a);
public delegate void DogHandler(Dog d);
DogHandler handler = new AnimalHandler(); // ✅ Contravariance
The delegate AnimalHandler
accepts an Animal
,
so it can handle a Dog
as well.
Real-World Use Cases
Variance is not just theoretical-it shows up in everyday C# programming. For example, the IEnumerable<out T>
interface is covariant. That means you can assign a
List<Dog>
to an IEnumerable<Animal>
:
List<Dog> dogs = new List<Dog>();
IEnumerable<Animal> animals = dogs; // ✅ Covariance
This works because IEnumerable<T>
only returns items-it never consumes
them.
Another example is IComparer<in T>
, which is contravariant. You can use
an IComparer<Animal>
to sort a list of Dog
objects:
IComparer<Animal> comparer = new AnimalComparer();
List<Dog> dogs = new List<Dog>();
dogs.Sort(comparer); // ✅ Contravariance
This works because the comparer only consumes T
-it doesn’t return it.
Rules and Limitations
Variance only works in interfaces and delegates-not in classes, structs, or methods. Also, you can’t use ref
or out
parameters with variant type
parameters. The compiler enforces these rules to prevent unsafe type conversions.
You also cannot use variance in generic methods. If you need variance, define it in the interface or delegate instead.
Use out
for covariance and in
for
contravariance in interfaces and delegates. This makes your APIs more
flexible and interoperable with inheritance hierarchies.
Design Tips for Using Variance
When designing your own generic interfaces or delegates, ask yourself:
- Does the type parameter appear only in return values? Use out
for covariance.
- Does the type parameter appear only in method parameters? Use in
for
contravariance.
- Does the type parameter appear in both input and output? Don’t use variance-it’s not allowed.
Keeping these rules in mind will help you design safe and flexible APIs that work well with polymorphism.
Summary
Covariance and contravariance describe how type substitution works in generic interfaces and delegates.
Covariance (out
) allows you to use a more derived type in output positions,
while contravariance (in
) allows you to use a more base type in input
positions. These concepts are essential for writing flexible, type-safe code that works with inheritance and
polymorphism. You’ve seen how they apply in interfaces like IEnumerable<T>
and IComparer<T>
, and
how to use them in your own designs. In the next article, we’ll explore Generic Delegates-how
to define and use delegates that work across types, and how they integrate with events, callbacks, and LINQ.