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
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)