Variances

Monday, August 31, 2015

How to use variant generics in C#

Intro

Covariance and contravariance have been around for quite a while. They refer to inheritance of complex types. It is present in many object oriented languages.

Let’s take the following code:

object[] arr = new string[10]; // Ok for the compiler.

arr[0] = "some string"; // Ok for the compiler and execution.
arr[2] = 1; // Ok for the compiler, execution error.

So the string[] is a descendant of object[] so write/read operations have become covariant. It will be as if we:

class ArrayOfObject {
    public virtual void Set(int index, object value) { /* ... */ }
    public virtual object Get(int index) { /* ... */ }  
}

class ArrayOfString: ArrayOfObject {
    public override void Set(int index, string value) { /* ... */ }
    public override string Get(int index) { /* ... */ }
}

This code won’t be valid in C#. The reason is obvious, it leads to execution errors. Somehow they allowed it for arrays.

Formally

Overriding function’s parameters or return type are said to be covariant if they have a more specific type than the one being overriden. They are said to be contravariant if it is the other way around.

It’s always safe to return a more specific type (covariance) and receive (parameters) a less specific one (contravariance).

C# allows variances on interfaces and delegates. So given:

class Base {}
class Desc: Base {}

Contravariant interfaces

interface IContravariant<in T> {
    void Foo(T t);
}

class Contravariant<T>: IContravariant<T> {
    public void Foo(T t) { /* ... */ }
}

IContravariant<Desc> c = new Contravariant<Base>();

Covariant interfaces

interface ICovariant<out T> {
    T Bar();
}

class Covariant<T>: ICovariant<T> {
    public T Bar() { /* ... */ }
}

ICovariant<Base> c = new Covariant<Desc>();

Contravariant delegate parameters

delegate void Contravariant<in T>(T t);

Contravariant<Base> boo = x => {};
Contravariant<Desc> foo = boo;

Covariant delegate return

delegate T Covariant<out T>();

Covariant<Desc> boo = () => default(Desc);
Covariant<Base> foo = boo;

Sum and wrap up

Covariance is safe for outputs, thus the out keyword, contravariance is safe in inputs, so it’s in keyword. The compiler will take care of no allowing variances if no modifier is applied to interface and delegates. It will also take care of allowing proper variance with proper modifiers. Finally it won’t allow using the wrong modifiers.