Generic interface co- and contravariance in C#

In this post, I will explore generic interface co- and contravariance in C# introduced in 4.0. which allow us to control how the inheritance of generic parameters affect the inheritance of their generic type itself.

Let’s start with a simple example: Suppose we have two classes, Person and Student and a container that implements a generic interface and holds a single item:

public class Person
{
	public string Name { get; set; }
}

public class Student : Person
{
	public string ID { get; set; }
}

public interface IContainer<T>
{
	T Item { get; set; }
}

public class Container<T> : IContainer<T>
{
	public T Item { get; set; }
}

If we construct a generic Container<Student>, we might think that this container inherits from IContainer<Person> because Student inherits from Person. After all, if a Student is a Person, a container holding a Student would also be a container holding a Person, right? The answer is actually no, though I would certainly make sense in the following example, where we pull a Student (and therefore a Person) out from the container:

IContainer<Student> studentContainer  = new Container<Student>();
studentContainer.Item = new Student();
IContainer<Person> personContainer = studentContainer;
Person person = personContainer.Item;

The problem is that we are also able to put a new Person into the container:

IContainer<Student> studentContainer  = new Container<Student>();
IContainer<Person> personContainer = studentContainer;
personContainer.Item = new Person();
Student student = studentContainer.Item;

This is clearly a problem as a person is generally not a student, and the above examples are therefore rejected by the compiler. So even though a Student is a Person, a Container<Student> is not a IContainer<Person>. We say that IContainer is invariant in regards to its type parameter – it does not ‘follow’ the inheritance of its type parameter.

Covariance

But what if we could somehow promise the compiler to only take stuff out of the container? Then we could let safely let Container<Student> inherit from IContainer<Person> without getting into trouble by e.g. inserting a Person into a Student container.

This type of inheritance is called covariance because the generic interface inheritance ‘follows’ its type parameter – that means that because Student inherits from PersonIContainer<Student> also inherits from IContainer<Person> (or a container of any other supertype). In C# done kind of variance is declared using the out keyword before the type parameter:

public interface IContainer<out T>
{
	T Item { get; }
}

This of course mean that we have to change our implementation so that it adheres to our new covariant interface:


Now we can safely let IContainer<Student> inherit from IContainer<Person>, and the following code compiles:

IContainer<Student> studentContainer = new Container<Student>();
IContainer<Person> personContainer = studentContainer;
Person person = personContainer.Item;

Contravariance

But what if we do it the other way around? We could promise the compiler that we only insert items into the container. Then we could safely treat our IContainer<Person> as a IContainer<Student> because we only insert students (which is also persons) into it and never get into trouble by pulling students out from a IContainer<Person>.

We do this by using the in keyword before the type parameter, and changes our implementation accordingly (we have to implement the set accessor as auto-generated accessors must have get accessors):

public interface IContainer<in T>
{
	T Item { set; }
}

public class Container<T> : IContainer<T>
{
	public T Item
	{
		set { }
	}
}

Now the following code will compile:

IContainer<Person> personContainer = new Container<Person>();
IContainer<Student> studentContainer = personContainer;
studentContainer.Item = new Student();

This kind of variance is called contravariance because we can treat a IContainer<Person> as a IContainer<Student> (or a container of any other subtypes). Hence the interfaces variates contra (opposite) its type parameter: Because a Student inherits from PersonIContainer<Person> inherits from IContainer<Student> (or any subtype).

Some real world examples

.NET includes some real-world examples of co- and contravariant generic interfaces. The IEnumerable<T> interface is covariant: It allows one to iterated over a collection, taking only stuff out of it. This means that the following code will compile:

IEnumerable<Student> students = new List<Student>();
IEnumerable<Person> persons = students;

Because Student inherits from Person, an IEnumerable<Student> inherits from IEnumerable<Person> – that is, is covariant.

An example of contravariant interfaces is the IComparer<T> interface that compares two objects of type T producing an interger. Because we only insert objects into the comparer, the interface is made contravariant, and the following code is allowed:

public class PersonCompare : IComparer<Person>
{
	public int Compare(Person x, Person y)
	{
		return (x.Name.CompareTo(y.Name));
	}
}

static void Main(string[] args)
{
	IComparer<Person> personComparer = new PersonCompare();
	IComparer<Student> studentComparer = personComparer;
}

This makes sense because if a compare can compare persons, it certainly can also compare students. Hence because Student inherits from Person, a IComparer<Person> inherits from IComparer<Student> – that is, is contravariant.