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.