Covariance, Contravariance and Invariance in C#

0
45

Introduction

Many programming language type systems support subtyping.  For instance, if Cat is subtype of Animal, then an expression of type Cat can be used whenever an expression of type Animal could. Variance refers to how subtyping between more complex types (list of Cats versus list of Animals, function returning Cat versus function returning Animal, …) relates to subtyping between their components. Depending on the variance of the type constructor, the subtyping relation may be either preserved, reversed, or ignored.

A programming language designer will consider variance when devising typing rules for e.g. arrays, inheritance, and generic datatypes. By making type constructors covariant or contravariant instead of invariant, more programs will be accepted as well-typed. On the other hand, programmers often find contravariance unintuitive, and accurately tracking variance to avoid runtime type errors can lead to complex typing rules. In order to keep the type system simple and allow useful programs, a language may treat a type constructor as invariant even if it would be safe to consider it variant, or treat it as covariant even when that can violate type safety.

The purpose of this article is to show how and when a derived class can be assigned to a base class and vice versa. The article is utilitarian to provide greater flexibility in assigning and using generic types. So, covariance and contravariance helps to solve paradoxical type error in methods and delegates.

When an object of a class does not support to be assigned in another class object as a parameter of a method and/or the return type of that method does not support to be assigned to an object of another class, In that case covariant and contravariant come into play.

Background

Within the type system of a programming language, a typing rule or a type constructor is:

  • covariant if it preserves the ordering of types (≤), which orders types from more specific to more generic;
  • contravariant if it reverses this ordering;
  • bivariant if both of these apply (i.e., both I<A>I<B> and I<B>I<A> at the same time);
  • invariant or nonvariant if neither of these applies.

The article considers how this applies to some common type constructors.

The article gives the answer of most commonly generated error like “Cannot implicitly convert type ‘X’ to ‘Y’. An explicit conversion exists (are you missing a cast?)”.

Here cast error means, you are violating the covariant and contravariant rule for the certain type of method’s argument.

Covariance: Assign Derived class to Base class. Enables you to use a more derived type than originally specified. You can assign an instance of IEnumerable<Derived> to a variable of type IEnumerable<Base>.

Contravariance:  Assign Base class to Derived class. Enables you to use a more generic (less derived) type than originally specified. You can assign an instance of IEnumerable<Base>to a variable of type IEnumerable<Derived>.

Invariance: Supports only the type of class originally specified. So it is neither covariant nor contravariant. You cannot assign an instance of IEnumerable<Base> to a variable of type IEnumerable<Derived> or vice versa.

C# examples

For example in C#, if Cat is a subtype of Animal, then:

  • IEnumerable<Cat> is a subtype of IEnumerable<Animal>. The subtyping is preserved because IEnumerable<T> is covariant on T.
  • Action<Animal> is a subtype of Action<Cat>. The subtyping is reversed because Action<T> is contravariant on T.
  • Neither IList<Cat> nor IList<Animal> is a subtype of the other, because IList<T> is invariant on T.

The variance of a C# interface is determined by in (contravariant) and out (covariant) annotations on its type parameters; the above interfaces are declared as IEnumerable<out T>, Action<in T>, and IList<T>. Types with more than one type parameter may specify different variances on each type parameter. For example, the delegate type Func<in T,out TResult> represents a function with a contravariant input parameter of type T and a covariant return value of type TResult.

The typing rules for interface variance ensure type safety. For example, an Action<T> represents a first-class function expecting an argument of type T, and a function that can handle any type of animal can always be used instead of one that can only handle cats.

Arrays:

Read-only data types (sources) can be covariant; write-only data types (sinks) can be contravariant. Mutable data types which act as both sources and sinks should be invariant. To illustrate this general phenomenon, consider the array type. For the type Animal we can make the type Animal[], which is an “array of animals”. For the purposes of this example, this array supports both reading and writing elements.

Should we treat this as:

  • Covariant: a Cat[] is an Animal[]
  • Contravariant: an Animal[] is a Cat[]
  • Invariant: an Animal[] is not a Cat[] and a Cat[] is not an Animal[]

If we wish to avoid type errors, then only the third choice is safe. Clearly, not every Animal[] can be treated as if it were a Cat[], since a client reading from the array will expect a Cat, but an Animal[] may contain e.g. a Dog. So the contravariant rule is not safe.

Conversely, a Cat[] cannot be treated as an Animal[]. It should always be possible to put a Dog into an Animal[]. With covariant arrays this cannot be guaranteed to be safe, since the backing store might actually be an array of cats. So the covariant rule is also not safe—the array constructor should be invariant. Note that this is only an issue for mutable arrays; the covariant rule is safe for immutable (read-only) arrays.

More regarding covariant and contravariant with some example code:

The List<T> class implements the IEnumerable<T> interface, so List<Derived> implements IEnumerable<Derived>. The covariant type parameter does the rest. Covariant type parameters enable you to make assignments that as shown in the following code.

IEnumerable<Derived> d = List<Derived>();
IEnumerable<Base> b = d;

in the above code segment ‘IEnumerable<Base> b=d’ assigns derived type to base type. And supports covariant type rule.

Contravariance, on the other hand, seems counterintuitive. The following example creates a delegate of type Action<Base> and then assigns that delegate to a variable of type Action<Derived>.

Action<Base> b = (target) => { Console.WriteLine(target.GetType().Name); };
Action<Derived> d = b;
d(new Derived());

in the above code segment, the statement ‘Action<Derived>d=b’ assigns base type to derived type. And supports contravariant parameter type rule.

List of variant generic interface and delegate types that support covariant and/or contravariant type parameters are showing in the image below:

for example, in the code below Base class method accepts an IEnumerable of Base type parameter. And Derived.PrintBases(dlist) statement inside the derived class Main method assigns Derive class instance of Enumerable to the Base class PrintBases method’s parameter. Thus it is a compliance of covariant rule. In the above list you can check that IEnumerable<T> accepts covariant type parameters.

class Base { public static void PrintBases(IEnumerable<Base> bases) { foreach (Base b in bases) { Console.WriteLine(b); } Console.Read(); } } class Derived : Base { public static void Main() { List<Derived> dlist = new List<Derived>(); dlist.Add(new Derived()); dlist.Add(new Derived()); dlist.Add(new Derived()); Derived.PrintBases(dlist); } }

Func<T, TResult> have covariant(Derived to Base) return types and contravariant(Base to Derived) parameter types. You can check in the above list that, Func<T,TResult> accepts contravariant parameter type and covariant return type. This is exactly shown in the code below ( I have also written some inline comments to apprehend the intended meaing of the code):

public class Type1 { }
public class Type2 : Type1 { }
public class Type3 : Type2 { } public class Program
{ public static Type3 MyMethod(Type1 t) { return t as Type3 ?? new Type3(); } static void Main() { Func<Type2, Type2> f1 = MyMethod; Func<Type3, Type1> f2 = f1; Type1 t1 = f2(new Type3()); }
}

Now let us consider a real example,

1. Let we have a Base class named ‘Animal’

2. Let ‘Cage’ is a derived class which derives ‘Animal’

3. Let ‘Deer’ and ‘Tiger’ are other derived classes that derive ‘Cage’ class.

The idea behind the above classes are to relocate Deer and Tiger using a cage. We have to be careful that, in any circumstance Deer and Tiger do no occupy the same cage at the same time. Otherwise a disaster will happen because Deer will be eaten by the Tiger and our purpose of relocating the animals will go in vein. So, we have to come up with a solution to load cage with one type of animal at a time then relocate them. And then occupy the same cage with another type of animal to relocate them safely.

Now, Let’s play with some code to accomplish the above idea and be happy at the attainment of nobel purpose !!! 🙂

public class Animal {}
public class Cage : Animal { }
public class Deer : Cage { }
public class Tiger : Cage { } public class ProgramAnimal
{ public static Cage CarryAnimal(Animal c) { if (c is Deer) return new Deer(); else return new Tiger(); } static void Main() { Func<Cage, Cage> animalCageDelegate = CarryAnimal; Func<Deer, Animal> animalDeerFunc = animalCageDelegate; Animal animalDeer = animalDeerFunc(new Deer()); Func<Tiger, Animal> animalTigerFunc = animalCageDelegate; Animal animalTiger = animalTigerFunc(new Tiger()); }
}

But one cannot do as below:

Func<Deer, Tiger> deer_tiger = animalCageDelegate

The above statement does not support contravariant parameter type and covariant return type of Func<T,TResult>.

If someone do so, then ‘Cannot implicitly convert type error will be shown’. It is not supported in covariance and contravariance rule, and interstingly not supported in our case either in the real life scenario since Tiger will eat Deer.

History

Released on 16th of May, 2017

Points of Interest

Knowledge on contravariance, covariance and invariance in C#

LEAVE A REPLY