C#
programming
interfaces
covariance
contravariance

Understanding Covariant and Contravariant interfaces in C

Master System Design with Codemia

Enhance your system design skills with over 120 practice problems, detailed solutions, and hands-on exercises.

Introduction

Covariance and contravariance in C# describe when a generic interface for one type can be used as an interface for another related type. These rules make APIs more flexible without giving up type safety, but only when the type parameter is used in restricted ways.

The short version is:

  • covariance means "more derived output is safe"
  • contravariance means "more general input is safe"

If that sounds abstract, the examples make it much clearer.

Invariant, Covariant, and Contravariant

Suppose Dog inherits from Animal. Even with that inheritance, List<Dog> is not automatically a List<Animal>. Generic types are invariant by default.

csharp
1class Animal { }
2class Dog : Animal { }
3
4List<Dog> dogs = new List<Dog>();
5// List<Animal> animals = dogs; // Not allowed

That restriction prevents unsafe writes. If a List<Dog> could be treated as List<Animal>, someone might add a Cat into it.

Variance loosens the rules only for interfaces and delegates whose type parameter usage is safe.

Covariance with out

Covariance is declared with the out keyword. It means the generic type parameter is only produced, not consumed. A covariant interface can return T, but it cannot accept T as a method parameter in positions that would allow unsafe input.

IEnumerable<out T> is the classic example:

csharp
IEnumerable<Dog> dogs = new List<Dog>();
IEnumerable<Animal> animals = dogs;

That conversion is safe because reading Dog values as Animal values cannot break anything.

You can define your own covariant interface like this:

csharp
1public interface IRepository<out T>
2{
3    T GetById(int id);
4}

Because T only appears in the return type, covariance is allowed.

Contravariance with in

Contravariance is declared with the in keyword. It means the interface consumes values of type T, but does not return them in a way that would require the specific runtime type.

IComparer<in T> is a common built-in example:

csharp
1IComparer<Animal> animalComparer = Comparer<Animal>.Create(
2    (left, right) => 0
3);
4
5IComparer<Dog> dogComparer = animalComparer;

This is safe because a comparer that knows how to compare any Animal can also compare two Dog objects.

A custom contravariant interface might look like this:

csharp
1public interface IProcessor<in T>
2{
3    void Process(T item);
4}

An IProcessor<Animal> can be used where an IProcessor<Dog> is required, because it can already handle a broader type.

Why the Restrictions Exist

Variance only works if the interface uses the type parameter safely.

For a covariant out T interface:

  • returning T is fine
  • accepting T as a method argument is not fine

For a contravariant in T interface:

  • accepting T is fine
  • returning T is not fine

If both reading and writing are needed, the type usually must stay invariant.

For example, this cannot be covariant:

csharp
1public interface IStorage<T>
2{
3    T Read();
4    void Write(T value);
5}

T is used in both output and input positions, so allowing variance would create unsafe assignments.

Reference Types Only

In C#, generic variance for interfaces and delegates applies only to reference types. Value types do not participate in these implicit variance conversions in the same way.

That is why examples usually use classes such as Animal and Dog instead of int and object.

Reading the Direction Correctly

Covariance often feels intuitive because it follows inheritance direction:

text
Dog -> Animal
IEnumerable<Dog> -> IEnumerable<Animal>

Contravariance goes the opposite way:

text
Animal -> Dog
IComparer<Animal> -> IComparer<Dog>

That reversal is what confuses most people at first. The key is to ask whether the interface produces values or consumes them.

Common Pitfalls

The most common mistake is expecting classes such as List<T> to be covariant. They are not, because they both read and write T.

Another issue is mixing up method return types with interface variance. The out and in keywords on generic parameters are about how the interface uses the type parameter, not about the parameter direction of the interface variable itself.

Developers also sometimes force casts where variance would have provided a safe conversion automatically. If an interface is already variant, prefer the natural assignment over runtime casting.

Finally, remember that variance does not make unrelated types compatible. It only applies when a real inheritance relationship already exists between the type arguments.

Summary

  • Generic interfaces in C# are invariant by default.
  • Use out for covariance when the type parameter is only returned.
  • Use in for contravariance when the type parameter is only accepted as input.
  • 'IEnumerable<out T> and IComparer<in T> are the canonical examples.'
  • Variance improves API flexibility, but only when the interface uses the type parameter in safe positions.

Course illustration
Course illustration

All Rights Reserved.