Covariance and contravariance

Before we begin, let’s define the simplest of classes/hierarchies for our test types…

The following will be used in the subsequent examples

class Base { }
class Derived : Base { }

With that out of the way, let’s look at what covariance and contravariance mean in the context of a programming language (in this case C#).

The covariant and contravariant described, below, can be used on interfaces or delegates only.

Invariant

Actually we’re not going straight into covariance and contravariance, let’s instead look at what an invariant generic type might look like (as we’ll be building on this in the examples). Invariant is what our “normal” generics or delegates might look like.

Here’s a fairly standard use of a generic

interface IProcessor<T> 
{
}

class Processor<T> : IProcessor<T>
{        
}

If we try to convert an IProcessor<Derived> to an IProcessor<Base> or vice versa, we’ll get a compile time error, i.e. here’s the code that we might be hoping to write

Now if we wanted to do the following

IProcessor<Derived> d = new Processor<Derived>();
IProcessor<Base> b = d;

// or

IProcessor<Base> b = new Processor<Base>();
IProcessor<Derived> d = b;

So, in the above we’re creating a Processor with the generic type Derived (in the first instance) and we might have a method that expects an IProcessor. In our example we simulate this with the assignment of d to b. This will fail to compile. Likewise the opposite is where we try to assign a IProcessor to an IProcessor, again this will fail to compile. At this point the IProcessor/Processor are invariant.

Obviously with the derivation of Base/Derived, this would probably be seen as a valid conversion, but not for invariant types.

With this in mind let’s explore covariance and contravariance.

Covariance

Covariance is defined as enabling us to “use a more derived type than originally specified” or to put it another way. If we have an IList<Derived> we can assign this to a variable of type IList<Base>.

Using the code

IProcessor<Derived> d = new Processor<Derived>();
IProcessor<Base> b = d;

we’ve already established, this will not compile. If we add the out keyword to the interface though, this will fix the issue and make the Processor covariant.

Here’s the changes we need to make

interface IProcessor<out T> 
{
}

No changes need to be made on the implementation. Now our assignment from a derived type to a base type will succeed.

Contravariance

As you might expect, if covariance allows us to assign a derived type to a base type, contravariance allows us to “use a more generic (less derived) type than originally specified”.

In other words, what if we wanted to do something like

IProcessor<Base> b = new Processor<Base>();
IProcessor<Derived> d = b;

Ignore the fact that b cannot possibly be the same type in this example (i.e. Derived)

Again, you may have guessed, if we used an out keyword for covariance and contravariance is (sort of) the opposite, then the opposite of out will be the in keyword, hence we change the interface only to

interface IProcessor<in T>
{
}

Now our code will compile.

What if we want to extend a covariant/contravariant interface?

As noted, the in/out keywords are used on interfaces or delegates, if we wanted to extend an interface that supports one of these keywords, then we would apply the keyword to the extended interfaces, i.e.

interface IProcessor<out T>
{
}

interface IExtendedProcessor<out T> : IProcessor<T>
{
}

obviously we must keep the interface the same variance as the base interface (or not mark it as in/out. We apply the keyword as above.

References

Covariance and Contravariance in GenericsCovariance and Contravariance FAQ
in (Generic Modifier) (C# Reference)
out (Generic Modifier) (C# Reference)