Understanding C# Covariance and Contravariance

The terms “Covariance” and “Contravariance” scare many developers, but understanding them is essential for designing flexible generic interfaces in C#.

Covariance (out)

Covariance allows you to use a more derived type than originally specified. In generics, it is denoted by the out keyword. It applies to return types.

public interface IProducer<out T>
{
    T Produce();
}

IProducer<string> stringProducer = ...;
// Valid! Because string IS An object.
IProducer<object> objProducer = stringProducer; 

Because T is only used as a return value (output), we know that whatever stringProducer returns (a string) is guaranteed to be an object.

Contravariance (in)

Contravariance allows you to use a more generic type than originally specified. It applies to input parameters, denoted by in.

public interface IConsumer<in T>
{
    void Consume(T item);
}

IConsumer<object> objConsumer = ...; // Defines Consume(object)
// Valid!
IConsumer<string> stringConsumer = objConsumer;

stringConsumer.Consume("hello");

This works because objConsumer can handle ANY object. So if we treat it as a consumer that only accepts strings, that is safe – strings are objects.

Why it Matters

This is extensively used in IEnumerable<out T> (why you can assign `List<string>` to `IEnumerable<object>`) and `Action<in T>`. Properly using `in` and `out` on your custom interfaces makes your libraries much more usable for consumers who need to deal with inheritance hierarchies.


Discover more from C4: Container, Code, Cloud & Context

Subscribe to get the latest posts sent to your email.

Leave a comment

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.