On Csharp - Covariance and Contravariance

Created on: 12 Jun 24 21:53 +0700 by Son Nguyen Hoang in English

Useful concepts for generic interface, delegates and more!

alt text

Recently I decided to teach myself some C# knowledge that I found my self lacking, including but not limited to Task (Concurrency), delegates and some Design Patterns. While reading books, I encountered two concepts that sound pretty weird. They are Covariance and Contravariance. What are these?

Problem
  • I didn’t find any good translation to Vietnamese of these two concepts.
  • The first result from Google indicates that Covariance is related to statistic?
  • So, what are they?
Definition

Below here my own definition for some keywords:

  • Deprived Type (from something): A data type that deprived (or inherited) from another type. E.g: string is deprived from object
  • Base Type (of something): A data type from which another type inheriting from. E.g object is basetype of string
  • Variance (to something): Having relation to another type (either as deprived type or base type)
  • Covariance: In English, prefix Co indicates mutual, join, or commons. In this context, contravariance is the ability to use subtype to the place supposed to belong in the base type
  • Contravariance: In English, prefix Contra indicates “opposite”. In this context, contravariance is the ability to use base type to the place supposed to belong in the deprived type.
  • Extra: Invariance indicates that the two type has no relations whatsoever.

We also provide a scenario to illustrate two types. Imagine there is a relation between two type: Animal and Dog. In which, Dog inherited from Animal.

alt text

Given these concepts predefined, let’s go to the details.

Special Keyword: in/out
  • in and out, has two meanings in c#:

    • The first situation you encountered them are for function parameters.
    • The second situation you encoutered them are for generic interface and generic delegates
  • Here, we focus on the later meaning. In the context of function, in and out implied the flow of information in respect to the function.

  • In the simplest example, we have Animal GetAnimal(string name){} in which Animal is the output, and string as input.

Covariance

Covariance implies the ability use a deprived type (subtype) to a place that supposed to be used for the basetype.

As it is an “ability”, not every code can have this. Array and some interface such as IEnumerable<T> support covariance. You yourself can craft your own generic interfaces that support Covariance using keyword out. Beside generic interfaces, other place you can use covariance (and contravariance) are delegate

For array, the most simple example could be

Animal[] animals = new Animal[10]{};
animals[0] = new Dog(); //valid

On Generic Interface, lets define some interface for better illustration.

public interface IAct<T> {
    public void DoSomething();
}

public AnimalImp : IAct<Animal> { //implements}
public DogImp : IAct<Animal> { //implements}

//Main Function:

IAct<Animal> iAnimal = new AnimalImp();
IAct<Dog> iDog = new DogImp();

The question is, can iAnimal assigned to iDog?

iAnimal = iDog; // Is this valid?

The answer is no. Because your generic interface is not covariant yet. To fix this, modify the IAct<> to be

public interface IAct<out T> {
    public void DoSomething();
}

iAnimal = iDog; // now this is valid

The keyword out also indicates one more thing: You cannot use T as the input in any function declared inside IAct. Basically it means

public interface IAct<out T> {
    public void DoSomething(T t); // this is not allowed
}
Why the rule?

Eric Lippert, one of developers who make compiler for C# gives this wonderful example

interface IR<out T>
{
    void D(T t);  //assuming this is valid
}

class C : IR<Mammal>
{
    public void D(Mammal m)
    {
        m.GrowHair();
    }
}
...
IR<Animal> x = new C();  // legal because T is covariant and Mammal is convertible to Animal
x.D(new Fish()); // legal because IR<Animal>.D takes an Animal

You can visit here to check his original answer on Stackoverflown.

Another example

In C#, one of the popular interface that leverages Covariance are IEnumerable<T> In reallity, give you check the Definition of that interface, here is what it shows:


namespace System.Collections.Generic
{
    //
    // Summary:
    //     Exposes the enumerator, which supports a simple iteration over a collection of
    //     a specified type.
    //
    // Type parameters:
    //   T:
    //     The type of objects to enumerate.
    public interface IEnumerable<out T> : IEnumerable
    {
        //
        // Summary:
        //     Returns an enumerator that iterates through the collection.
        //
        // Returns:
        //     An enumerator that can be used to iterate through the collection.
        IEnumerator<T> GetEnumerator();
    }
}

Behind the sugar-syntax is an out keyword under the hood!

Contravariance

Contravariance is opposite ("contra") to Covariance that it allowed the less specific type (Animal) to the place that is supposed for specific type Dog. Similar to Covariance, generic interface that supports contravariance go with special keyword in.

public interface IAct<in T> {
    public void DoSomething(T instance);
}

public AnimalImp : IAct<Animal> { //implements}
public DogImp : IAct<Animal> { //implements}

As we have in in the interface, below code are valid:

IAct<Animal> animal = new AnimalImp();
IAct<Dog> dog = new DogImp();

dog = animal //This is valid!

T in contravariance cannot in the output position when in keyword comes with T. Why this rule is enforced?

Why the rule?

Similar to Eric’s example, here we have another example to illustrate.

interface IR<in T>
{
    T D( t); //assuming this is valid
}

class Fish : Animal {}
class Mammal : Animal {}

class C : IR<Animal>
{
    public Animal D()
    {
        return new Mammal(); // valid because Mammal inherited from Animal
    }
}
IR<Fish> fish = new C(); //valid? But now Fish having a Mammal instance inside 
Another example

In C#, one of the popular interface that leverages Contravariance are IComparer<T>. Similar to the IEnumerable<T>, I provide the definition of interface in c#:

namespace System.Collections.Generic
{
    //
    // Summary:
    //     Defines a method that a type implements to compare two objects.
    //
    // Type parameters:
    //   T:
    //     The type of objects to compare.
    public interface IComparer<in T>
    {
        // comment has been discarded
        int Compare(T? x, T? y);
    }
}

You can find the in keyword!

Extra.

Below declarations are totally valid:

public interface IMyInterface<in T, in T1, out T2, out T3 ...> //this is valid

So you can mix-match the output/input for the function as you wish!

public void Test1(T1 t1);
public T2 void Test2(T1 t1, T t);
Further Discussion
Support both Contravariance and Covariance on same Type?
  • It could be wild to think an interface that support both Contravariance and Covariance in a type.
  • Assuming this exist we would have:
IAct<Animal> animal = new AnimalImp();
IAct<Dog> dog = new DogImp();
IAct<Fish> fish = new FishImp();

// If this is allowed, then:
dog = fish

What exactly we would have here? We basically make a feature that allow any type to be convertible to any other type as long as they have same basetype.

Apply this thinking to IAct<object>, IAct<Exception> and IAct<string>. This would means IAct<string> can be converted to IAct<Exception>

Again, Eric Lippert answered the question here.

What about Covariance and Contravariance on Generic Class?

Assuming you can do this

public class Animal<in T> {
    T CreateT() // this would violate the keyword "in"
}

Basically, if this possible, you cannot create any getter for T in Animal. Is this a good thing?

One more time, Eric Lippert answered this concern here.

Bonus: On Delegate

This is not supposed to be a post on delegate, but unfortunately the topic of Covariance and Contravariance related to delegate so much, as in and out are used extensively in delegate and other built-in delegate type like Action and Func.

Covariance on delegate

The code below are taken from here

// Type T is declared covariant by using the out keyword.  
public delegate T SampleGenericDelegate <out T>();  
  
public static void Test()  
{  
    SampleGenericDelegate <String> dString = () => " ";  
  
    // You can assign delegates to each other,  
    // because the type T is declared covariant.  
    SampleGenericDelegate <Object> dObject = dString; //valid
}

As in generic interface rule, T cannot in input position.

Contravariance on delegate

We can easily craft a similar example

public delegate void NewSampleGenericDelegate<in T>();

public static void Test()  
{  
    NewSampleGenericDelegate<string> sDelegate = delegate() {
        // Do Nothing here
    };
    NewSampleGenericDelegate<object> objDelegate = delegate() {
        // Do Nothing here
    };

    sDelegate = objDelegate; // valid

}
As in `generic interface` rule, `T` cannot in *output* position.

Action and Func

Behind the scene, Action and Func are basically delegate under sugar syntax. The difference is that Action return value while Func are not. They are basically predefined delegate.

Checking the definition of Action from this link we have:

public delegate void Action<in T>(T obj);
public delegate void Action<in T1,in T2>(T1 arg1, T2 arg2);

...

// It support at most 16 parameters.

Because they are basically delegate with in, Action supports contravariance:

public static Action<Dog> MyAction;

public static void Demo2(){

    void H(Animal animal){
        // do something
    }
    MyAction = H; // Valid
}

In the addition, under the hood of Func there is something similar:

public delegate TResult Func<in T,out TResult>(T arg);
public delegate TResult Func<in T1,in T2,out TResult>(T1 arg1, T2 arg2);

...

// It support at most 16 parameters for input and 1 parameter for output.

To illustrate covariance and contravariance for Func, we provide some example:

static Func<string, Animal> myFunc;
public static void Main3()
{
    Dog CreateDog1(string s) => new Dog();
    Dog CreateDog2(object s) => new Dog();

    myFunc = CreateDog1; // valid
    myFunc = CreateDog2; // valid
}
End

That’s it for my short analysis. This is the first time I encountered this concept so misunderstanding can happens. If this article contains any error then please let me know.

Best Regards.

P/S: If you are Vietnamese and you know good translations for these two concepts then please let me know.

Acknowledgement
  • Thanks for Eric Lippert who answer so many questions on Stackoverflow. I will definitely visit his blog someday.
  • User Rotaro and cathei in C# discord channel who patiently help me to understand this concept. You can join the channel here
Back To Top