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.
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:
That conversion is safe because reading Dog values as Animal values cannot break anything.
You can define your own covariant interface like this:
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:
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:
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
Tis fine - accepting
Tas a method argument is not fine
For a contravariant in T interface:
- accepting
Tis fine - returning
Tis not fine
If both reading and writing are needed, the type usually must stay invariant.
For example, this cannot be covariant:
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:
Contravariance goes the opposite way:
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
outfor covariance when the type parameter is only returned. - Use
infor contravariance when the type parameter is only accepted as input. - '
IEnumerable<out T>andIComparer<in T>are the canonical examples.' - Variance improves API flexibility, but only when the interface uses the type parameter in safe positions.

