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 outoutcovariant

  • A consumer takes in things of type T → values flow inincontravariant

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.

Next
Next

What .NET and the CLR Really Are: A Beginner-Friendly Explanation