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.