Co/Contravariance in C# Interfaces

Covariance and contravariance are complicated words for describing behaviours that arise from simple concepts. In this post I'll unpack what they mean and explain how they apply to generic interfaces in C#.
For someone learning this topic, I think those big, scary words are unhelpful. They encourage the feeling that this is highly complex topic. But it is not. It's all centred around a basic concept in C# – implicit reference conversions.
Implicit Reference Conversions
You will have no doubt encountered this sort of code many times
public class Shape(string colour)
{
public virtual int Area { get; } = 0;
public string Colour { get; set; } = colour;
}
public class Square(string colour, int length): Shape(colour)
{
public override int Area { get; } = length * length;
public int Length { get; } = length;
}
public static void PrintArea(Shape shape) => Console.WriteLine($"Area of shape: {shape.Area}");
Square square = new ("Blue", 2);
PrintArea(square); // Shows: Area of shape: 4
It defines a base class, Shape
, a derived class, Square
, and a function, PrintArea
, that takes as argument a reference of type Shape
. The final two lines create a new Square
instance and use it to invoke the function. And despite the function declaring a single parameter of type Shape
, this code still compiles. It works because there exists an implicit reference conversion from a reference of type Square
to a reference of type Shape
. The conversion exists because the class Square
derives from Shape
.
Implicit reference conversions are the mechanism that enable the is a relationship between types and the types they derive from and the interfaces they implement (this would have worked the same if we would have used an interface IShape
instead of the base class Shape
). So when we supply a Square
object as argument for the parameter shape
, it works because a Square
is a Shape
– the compiler implicitly converts it to a reference of type Shape
. The actual object remains the same, the reference is just interpreted differently.
It works the same way with interface inheritance. Let's say we have an interface ISquare
that derives from IShape
, then a reference of type ISquare
is implicitly convertible to one of type IShape
.
public interface IShape
{
int Area { get; }
string Colour { get; set; }
}
public interface ISquare : IShape
{
int Length { get; }
}
public class Square(string colour, int length) : ISquare
{
public string Colour { get; set; } = colour;
public int Length { get; set; } = length;
public int Area => this.Length * this.Length;
}
ISquare blueSquare = new Square("Blue", 2);
IShape square = blueSquare;
The last line declares a variable, square
, of type IShape
and assigns a reference to the Square
object constructed on the line above. This triggers an implicit reference conversion from ISquare
to IShape
.
By the way, these examples would have worked the same were they generic types. The following example demonstrates implicit reference conversions between derived generic types and their generic base types.
public interface IBase<T>
{
void BaseMethod(T t);
}
public interface IDerived<T> : IBase<T>
{
void DerivedMethod(T t);
}
public class Derived<T> : IDerived<T>
{
public void BaseMethod(T t)
{
Console.WriteLine("BaseMethod");
}
public void DerivedMethod(T t)
{
Console.WriteLine("DerivedMethod");
}
}
Derived<int> derived = new();
IDerived<int> iderived = derived;
IBase<int> ibase = iderived;
In each of these examples, an implicit reference conversion between two types exists because one derives from another (directly or indirectly). But in some cases, implicit reference conversions between generic interfaces arise in a different way. Sometimes references of a generic interface type are implicitly convertible to another when there exists a conversion between their type arguments. There are two different flavours of this and they're named covariance and contravariance.
Covariance
Instead of a method that prints the area of a single shape, consider a method that prints the area of each shape in a collection of shapes. The PrintAreaOfShapes()
, defined below, does this, it works in terms of the generic interface IEnumerable<T>
.
public static void PrintAreaOfShapes(IEnumerable<Shape> shapes)
{
foreach (Shape shape in shapes)
{
Console.WriteLine($"Area of shape: {shape.Area}");
}
}
Earlier we saw that we could pass a reference of type Square
as an argument to a parameter of type Shape
. An obvious question arises: can we do the same sort of thing with the PrintAreaOfShapes
method – providing an IEnumerable<Square>
as an argument for the parameter of type IEnumerable<Shape>
?
In other words: does there exist an implicit reference conversion from IEnumerable<Square>
to IEnumerable<Shape>
because there is one from Square
to Shape
?
It turns that there does. The following code compiles:
IEnumerable<Square> squares = [new Square("Blue", 2), new Square ("Red", 4)];
PrintAreaOfShapes(squares);
Interesting. What happens if we define the function to work in terms of ICollection<Shape>
instead of IEnumerable<Shape>
?
public static void PrintAreaOfShapes(ICollection<Shape> shapes)
{
foreach (Shape shape in shapes)
{
Console.WriteLine($"Area of shape: {shape.Area}"
}
}
This code does not compile...
ICollection<Square> squares = [new Square("Blue", 2), new Square ("Red", 4)];
PrintAreaOfShapes(squares);
So, where a type, Derived
, derives from a base type, Base
, there exists an implicit reference conversion from IEnumerable<Derived>
to IEnumerable<Base>
, but no conversion exists from ICollection<Derived>
to ICollection<Base>
.
Why is that - why does it work with one but not the other?
First, let's consider why the compiler cannot allow an implicit reference conversion from ICollection<Derived>
to ICollection<Base>
.
The ICollection<T>
interface is designed to represent a modifiable collection – it has an Add
method, for example, which adds an item of type T
to the collection. If the compiler were to allow an implicit reference conversion from ICollection<Derived>
to ICollection<Base>
consider what that would mean for the following method:
public static void AddBase(ICollection<Base> bases)
{
bases.Add(new Base());
}

This function takes a reference to a collection of Base
objects through a reference of type ICollection<Base>
and adds a new Base
object to the collection by invoking the Add
method. That means, if you were to pass an ICollection<Derived>
to this method, it would be adding a Base
object to a collection of Derived
objects. Any code working with the original ICollection<Derived>
object expects every object in the collection to be a Derived
. But if the code above were to run on an ICollection<Derived>
, the collection would contain a mix of Derived
and Base
instances, and the expectation would be defeated. Type safety would be lost, so this clearly cannot be allowed.
The point here is that ICollection<T>
has a method with a T
typed parameter – it allows T
s to be passed in. And that is the reason why an implicit reference conversion from ICollection<Derived>
to ICollection<Base>
cannot be allowed.
You might be thinking: "But the PrintShapeAreas
method doesn't invoke the Add
method, so why does the compiler care?". The reason is that the compiler cannot always know what code is inside a method. Its rules are, therefore, based on the method signature rather than the contents. So it just sees a parameter of type ICollection<T>
, with a method, Add
, that takes a T
. And It knows that's a situation in which it's possible for the type system to be broken, so it rejects the code.
Going back to IEnumerable<T>
. Why does an implicit reference conversion from IEnumerable<Derived>
to IEnumerable<Base>
exist? There does because IEnumerable<T>
does not have any methods that take a T
, it has a single method, MoveNext
, that takes zero arguments that returns a T
. In other words, you cannot put T
s into an IEnumerable<T>
, you can only get T
s out.
Given that, if you've got an IEnumerable<Derived>
, you can only get Derived
objects out. But since Derived
is implicitly convertible to Base
, each Derived
object is also a Base
, so you have effectively got an IEnumerable<Base>
. Therefore an IEnumerable<Derived>
is effectively an IEnumerable<Base>
.
IEnumerable<Derived> ds = [new Derived(), new Derived()];
foreach(Derived d in ds)
{
Base b = d // A Derived *is a* Base
}
This feature of IEnumerable<T>
is called covariance. IEnumerable<T>
is said to be covariant in its type parameter T
.
However, just because IEnumerable<T>
meets the conditions for being covariant, that doesn't automatically make it covariant. It is so because IEnumerable<T>
is explicitly defined to be so. The relevant part of its definition looks like this
public interface IEnumerable<out T> : System.Collections.IEnumerable
The word out
next to the type parameter declares the interface as being covariant in that type parameter. The use of the word "out" makes sense since you must only be able to get T
s out of an interface for it to be covariant. Also, you cannot define any old interface as being covariant as the compiler checks that it meets the conditions described.
Contravariance
We have saw how covariant interfaces represent objects that only allow values to come out from them. You may have guessed that contravariance has to do with interfaces that only allow values to go into them.
With covariance, the values taken out are implicitly convertible to references of a base type, i.e. Derived
--> Base
. With contravariance, the values passed in are also implicitly convertible to a base type. This reverses the direction for conversions between the interface types, as we'll see.
Let's go back to the ICollection<T>
example from earlier and consider if it should be possible to pass an ICollection<Shape>
where an ICollection<Square>
is expected. That is, does an implicit reference conversion from ICollection<Base>
to ICollection<Derived>
exist? (Note: this is the opposite direction to what was considered earlier.)
public static void PrintAreaOfSquares(ICollection<Square> shapes)
{
foreach (Square shape in shapes)
{
Console.WriteLine($"Area of shape: {shape.Area}"
}
}
ICollection<T>
derives from IEnumerable<T>
and consequently inherits MoveNext
, which returns a T
. So, ICollection<T>
has methods that allow T
s to come out as well as methods that allow T
s to go in.
In this case it's much more obvious why we cannot pass an ICollection<Shape>
, but let's consider what would happen if we could. The foreach
loop – which implicitly invokes MoveNext
– is expecting each item to come out of the collection to be a Square
, but they're in-fact Shape
objects, and there is no implicit reference conversion from Shape
to Square
.
This is effectively the same as attempting the following
IEnumerable<Derived> bs = [new Base(), new Base()];
foreach(Base b in bs)
{
Derived d = b // Error - cannot implicitly convert Base to Derived
}
So the compiler cannot allow this conversion. The problem is that ICollection<T>
provides methods that allow T
s to come out of it. Next, let's see a concrete example of a contravariant interface.
The canonical example of a contravariant interface is IComparer<T>
. It defines a single method, Compare
, for comparing two objects of the same type:
public int Compare(T x, T y)
I.e. this is an interface that only allows objects to go into it.
So, should an object of type IComparer<Base>
be implicitly convertible to IComparer<Derived>
? Can we think of any reason why not?
If it were, the compare method signature would go from
public int Compare (Base x, Base y)
to
public int Compare (Derived x, Derived y)
Once we have passed instances of Derived
to parameters x
& y
, they can be implicitly converted to Base
type references because a Derived
is a Base
. Therefore an IComparer<Base>
is effectively an IComparer<Derived>
.
Let's look at a concrete example of this using the Shape
and Square
types from previous examples. The example below defines the ShapeComparer
class that implements IComparer<Shape>
with the logic that two Shape
instances are considered equal if their Area
is equal.
public class ShapeComparer : IComparer<Shape>
{
public int Compare(Shape x, Shape y)
{
return x.Area.CompareTo(y.Area);
}
}
We can create an instance of ShapeComparer
and assign it into a reference of type IComparer<Square>
:
IComparer<Shape> shapeComparer1 = new ShapeComparer()
IComparer<Square> shapeComparer2 = shapeComparer1;
This makes sense because the Compare
method takes two Shape
references and since any Square
is a Shape
, it's acceptable to pass Square
references. Looking at the implementation, Compare
uses the Area
property – a property common to all Shape
types.
As with covariance, you don't get the feature by accident. If your interface satisfies the conditions for contravariance and you want to make it contravariant, you must explicitly do so using, unsurprisingly, the word in
. The IComparer<T>
interface is said to be contravariant in its type parameter T
. Here's the relevant part from its definition:
public interface IComparer<in T>
By the way, you will sometimes hear people talking about generic interfaces being variant. If an interface is defined as covariant or contravariant, it is said to be variant, otherwise it is said to be invariant. Therefore IEnumerable<T>
and IComparer<T>
are variant, whilst ICollection<T>
is invariant.
What's with the words covariant and contravariant?
The words are borrowed from mathematics; the most directly relevant example comes from category theory, but they're also used in the study of tensors and in statistics. "Variance" refers to a change or transformation, "co" means "together" (as in "cooperative"), and "contra" means "opposite" or "against" (as in "contradiction"). Therefore, covariance describes changes in the same direction, whilst contravariance describes changes in the opposite direction. Those descriptions align with the behaviour of IEnumerable<T>
and IComparer<T>
discussed above.
There is an implicit reference conversion from IEnumerable<T1>
to IEnumerable<T2>
when there exists an implicit reference conversion between T1
and T2
. This is named covariance because implicit reference conversions between the interfaces work in the same direction as conversions between the type parameters.
For IComparer<T>
, there is an implicit reference conversion from IComparer<T1>
to IComparer<T2>
when there exists an implicit reference conversion from T2
to T1
. This is named contravariance because the implicit reference conversions for the interfaces work in the opposite direction to conversions for the type parameters.
Wrapping Up
Co/contravariance are names given to special rules that enable implicit reference conversions between certain generic interfaces when implicit reference conversions for their type parameters exist. The key to understanding the rules is to remember the key components: implicit reference conversions (between the type arguments) and whether the interfaces only allow items to go into them or only allow items to come out from them.
Covariance enables conversions between references of interface types in the same direction as the type arguments because they only allow items to come out from them. Contravariance enables conversions between interface types in the opposite direction to the type arguments because they only allow items to go into them.