Covariance and Contravariance In 5 Minutes

C# 4.0 will introduce new language features to help manage covariance and contravariance.  While this is a a wonderful capability, it forces us to come to terms with these (to me) somewhat unfamiliar terms.

As I have been working through Eric Lippert’s excellent series of posts on this topic as well as the wikipedia entry, it has occurred to me that the best way to come to grips with these concepts is to examine gradually more complex examples of covariance and contravariance as it currently exists in C# 3.  If this appears to be a rehash of Eric Lippert’s work, this is probably because it is.  Consequently, any mistakes are purely mine, while any virtues in this explanation are clearly his.

First, however, it is necessary to learn some vocabulary.

Take the following class hierarchy:

        public class Animal{}
        public class Mammal : Animal { }
        public class Tiger : Mammal { }

We would typically say that Mammal is more derived than Animal.  Mammal, conversely, is less derived than Tiger in this inheritance chain.

To understand covariance and contravariance, we need to replace more derived and less derived with the metaphorical terms smaller and bigger.

We do this because more derived and less derived are not adequate to describe the relation between objects such as arrays of types.

        Animal[] A;
        Mammal[] M;
        Tiger[] T;

An array of Mammal does not derive from an array of Animal.  Both Mammal[] and Animal[] are derived from System.Array. There is no inheritance relation, however, between Mammal[] and Animal[] themselves.  Nevertheless there is obviously some sort of relation present.  By convention, we call this relation a smaller than \ greater than relation.  Mammal[] is smaller than Animal[] because Mammal is more derived than Animal.  It is bigger than Tiger[] because Mammal is less derived than the type Tiger.

It is worth repeating that smaller and bigger are not the same thing as the relation between classes in an inheritance hierarchy.  They also do not have anything directly to do with memory management.  They are pure metaphors.

The C# classes above model concepts.  When we talk about concepts such as animal, mammal and tiger, we talk about these concepts falling under each other.  Socrates falls under the concept of man.  Man falls under the concept of an animal (traditionally, man is defined as a bipedal, hairless animal that laughs; see Haecceity).

Metaphorically, we can think of animal as a bigger concept than mammal because many more things fall under animal than under mammal.  Tiger, in turn, is a smaller concept than mammal because so few things fall under it.

Once we have smaller than and bigger than under our belts, we can start talking about covariance and contravariance.  Covariance and contravariance, at the most basic level, concerns the assignment of types to variables.

An assignment is covariant if we can assign a smaller type to a bigger type:


If we can assign a bigger type to a smaller type, the assignment is said to be contravariant:


1. Covariance with simple reference types

An example will help to illustrate this. 

    Mammal m = new Tiger();
    Animal a = m;
    //Tiger t = a;  -- will not compile

We can assign a smaller type, Tiger to a variable allocated as a Mammal.  Similarly we can assign our Mammal instance to a variable allocated as an Animal.  In other words, in the assignment of simple reference types, we can assign smaller things to bigger things.  This is a covariant relation.

We cannot, however, assign an Animal instance to a Tiger.  The C# compiler just won’t let us do it, because it will not let us assign something bigger to something smaller in this case.  The assignment of simple reference types is therefore not covariant.

2. Covariance with arrays

The C# compiler also happens to allow covariant array assignments.  For instance:

    Mammal[] M = new Tiger[0];
    Animal[] A = M;
    //Tiger[] t = A;  -- will not compile

The compiler doesn’t need to allow this.  It just happens to.  What’s interesting is that the compiler will not allow us to do the same thing with generic collections. 

3. Invariance with generics

Assignment of generics is invariant in C# 3.

    // List<Mammal> mammals = new List<Tiger>(); -- will not compile
    // List<Animal> animals = mammals; -- will not compile
    // List<Tiger> tigers = animals;  -- will not compile

4. Covariance with method parameters

Parameter assignment also happens to be covariant in C#.  To prove it to yourself, take the following static method:

     public static void TakeMammal(Mammal m){}

Which of the following operations is permissible in C# and which is not?

    TakeMammal(new Tiger());

    TakeMammal(new Animal());

    TakeMammal(new Mammal());

(Note, however, that ref and out parameters are invariant: .)

5. Delegate contravariance with respect to inputs

Understanding this characteristic of parameter assignments is important in order to demonstrate how contravariance occurs in C#.  In C#, delegate assignments are contravariant with respect to inputs.  Consider this code:

 public static void TakeAnimal(Animal a) {}
 public static void TakeMammal(Mammal m){}
 public static void TakeTiger(Tiger t) {}

public delegate void MammalHandler(Mammal m);

MammalHandler funcA = TakeAnimal; //OK
MammalHandler funcM = TakeMammal; //OK
MammalHandler funcT = TakeTiger;  //No!

            funcA(new Mammal()); //OK
            funcM(new Mammal()); //OK
            funcT(new Mammal()); //No!

The delegate method MammalHandler is designed to only take the Mammal type as input.  Any delegate instance – funcA, funcM, funcT – in turn may be passed Mammal objects.  If we pass a Mammal to delegate instance funcA, it ultimately gets passed to our TakeAnimal method.  Since parameter assignments – as we showed above – are covariant, passing a Mammal to TakeAnimal is permissible.  Obviously passing a Mammal to TakeMammal is also permissible.

We cannot, however, pass a Mammal to TakeTiger.  The C# compiler does not allow this.

Consequently, we also cannot assign TakeTiger to our MammalHandler delegate.  Assigning TakeTiger to funcT is illegal.

But in this impermissible delegate assignment, which thing is bigger: MammalHandler or TakeTiger?  MammalHandler, right?  With regard to delegate assignments, then, we are saying that assigning something smaller to something bigger is not allowed.  Assigning something bigger to something smaller, however, is permissible.

Delegate assignments like those above are therefore contravariant.

6. Delegate covariance with respect to return types

I say “like those above” because this is actually true only with respect to delegate inputs.  Delegates can also, of course, return objects – and when they do, this is a covariant relation.  Delegate assignments are said to be contravariant with respect to inputs, but covariant with respect to return types.  You can prove this to yourself by looking through the following code in which the delegate signature has a return value but no parameters.


public static Animal GetAnimal() {} public static Mammal GetMammal(){} public static Tiger GetTiger() {} public delegate Mammal GetMammalDelegate(); GetMammalDelegate funcA = GetAnimal; // No! GetMammalDelegate funcM = GetMammal; // OK GetMammalDelegate funcT = GetTiger; // OK




So why is this important for understanding C# 4.0?  As we briefly covered above, C# 3 has this peculiar characteristic in that the compiler treats array assignments as covariant operations, but treats the generic assignment of List<T> as an invariant operation. This is obviously a little weird.  In C# 4.0, there will be support for covariance and contravariance in generic interfaces which will allow us to specify how we want our generics to behave – for instance, a special kind of covariant IEnumerable<out T> could be implemented that would provide the sort of covariance now supported for arrays.

2 thoughts on “Covariance and Contravariance In 5 Minutes”

Leave a Reply

Your email address will not be published. Required fields are marked *