NDepend

Improve your .NET code quality with NDepend

Covariance and Contravariance in C# Explained

Covariance and Contravariance in CSharp Explained

Introduction

Covariance and contravariance allow more flexibility when dealing with C# class hierarchy. This article explains and demonstrates the concepts of Covariance and Contravariance in C# .NET. These concepts will be first introduced for generics, then for delegates and finally for arrays.

Before going further, let’s briefly remind some basic OOP notions as a quick refresher. C# has a relationship defined for types and objects in the form of Inheritance, Polymorphism, and subtyping.

We will take a simple example as shown below,

Here are some remarks:

  • Inheritance:  CheckingAccount and SavingAccount both inherit from base class Account.
  • Subtyping :  A CheckingAccount reference or a SavingAccount reference can be assigned to an Account reference thanks to subtyping principles. This remark has two opposite consequences:
  • A) If a method has an input parameter of type Account, the caller can pass an instance of SavingAccount or CheckingAccount as input
  • B) If a method has an Account return type, it can return a SavingAccount object or a CheckingAccount object. If this method is virtual and is overridden in a derived class, the override method can return an Account reference. Alternatively since C#9 the override method can also safely returns a SavingAccount or a CheckingAccount reference, the code below is valid. This is a wonderful new feature of C#9 named Covariant Return Type:

We just get a taste of variance that enables implicit reference conversion for generic parameter type, delegate and array by relying on the subtyping relationship. The word implicit is in bold in the previous sentence because this is what variance is about. The picture below explains why the Covariance and Contravariance implicit conversion seamlessly works for in and out parameters.

CSharp Covariance and Contravariance

Mnemonic: just remember the n: Contravariance is about in parameters. Consequently Covariance is about out parameters.

Covariance in Generics

Covariance and Contravariance in C# generics provide greater flexibility in assigning and using generic types.

Covariance applies to generic parameters type used as method return type. The type parameter T of the IEnumerable<out T> is covariant because it is declared with an out keyword. This means that an IEnumerable<Derived> reference can be assigned to an IEnumerable<Base> reference:

IEnumerable<CheckingAccount> assignment to IEnumerable<Account> is valid because through IEnumerable<T>, elements of type T are always gathered-out and never assigned-in. Thus there is no risk of assigning a SavingAccount object to a CheckingAccount reference for example.

This covariance feature appeared with C# 4. One of the motivator was to remove the annoying calls to Cast<T>() as explained in the comment above. Notice that introducing covariance was a breaking change. Indeed, in the code sample below, before C#4 Method2() could be called. But since C#4 covariant there is no way it can be called.

Contravariance in Generics

Contravariance is enforced in relation to a particular generic type using in the generic modifier. Contravariance applies to generic parameters type used as method parameter. A good example from the Base Class Library (the BCL) is IComparer<in T>.

In the code sample below the class AccountComparer implements IComparer<Account>. But because IComparer<in T> is declared with in, T is contravariant. This is why a IComparer<Account> reference can be assigned to a IComparer<CheckingAccount> reference.

IComparer<Account> assignment to IComparer<CheckingAccount> is safe because within the context of the interface IComparer<in T>, elements of type T are always assigned-in and never gathered-out. Thus there is no risk of gathering a SavingAccount object from a IComparer<CheckingAccount> comparer for example.

As a rule of thumb:

  • A covariant generic type parameter (out) can be used as methods return type.
  • A contravariant generic type parameters (in) can be used as methods parameter types.
  • Variance of multiple generic type parameters are independent. A generic interface or generic delegate type can have both covariant and contravariant type parameters.

Hopefully the compiler is here to check as illustrated by the screenshot below:

C#-Compiler-Check-Variance

Covariance and Contravariance in Delegates

Covariance and Contravariance in C# provide flexibility for matching a delegate type with a method signature.

  • Covariance permits a method to have a return type that is a subtype of the one defined in the delegate.
  • Contravariance permits a method to have a parameter type that is a base type of the one defined in the delegate type.

Here is an example of delegate covariance:

It is important to note that in the BCL the Func<…>  and Action<…> generic delegates are defined as below,

  • Func<T, TResult>  to Func<T1,T2,…T16,TResult> have a covariant return type and contravariant parameter types. Thus these generic delegates are declared this way with the in and out keywords:

  • The same way Action<T> to  Action<T1, T2,….T16> have contravariant parameter types.

With contravariance behavior, you can assign an instance of Action<Account> to a reference of type Action<CheckingAccount>. It works because the parameter target can be any Account object and consequently, it can be any CheckingAccount object.

Here is a more concrete example of delegate contravariant parameter. The MultiHandler() method can accept as a parameter any KeyEventArgs object. Thus it can accepts any MouseEventArgs.

Finally here is a delegate Func<TResult> covariance example. func2 can return any Account and thus it can return any CheckingAccount!

Covariance in Array

C# has support for array covariance. Thus in C# an array of CheckingAccount is an array of Account. Unfortunately this is not type-safe as demonstrated by the example below:

ArrayTypeMistmatchException

array1:CheckingAccount[] being assigned to array2:Account[] compiles fine thanks to covariance but leads to an ArrayTypeMistmatchException at runtime. The runtime checks that a SavingAccount object cannot be assigned in a slot of an array of CheckingAccount.

Notice that the covariance can become type-safe this way (see below). This is because IEnumerable<out T> is read-only, there is no in parameter. Consequently there is no risk to update an element in the sequence:

Contravariance in Array

C# doesn’t support implicit contravariance on array. This code doesn’t compile:

C# array contravariance

An explicit cast is supported by the compiler but it leads to an InvalidCastException at runtime!

C# array contravariance InvalidCastException

Conclusion

We’ve seen how the concepts of Covariance and Contravariance are implemented through C# generics, delegates and arrays. These should be prohibited on array since this can lead to non-trivial runtime exceptions. However the C# generic and C# delegate implementations of variance concepts are clean and safe. Concretely this feature really prevents boilerplate code like some annoying calls to Cast<T>(). There are chances that you have been using this feature without even noticing. The important thing to remember is that:

  • The keyword in is used for generic type parameters that type objects that are only passed-in as parameter.
  • The keyword out is used for generic type parameters that type objects that are only gathered-out as result.

As simple as that! However it is recommended to continue using scientific terms like Covariance and Contravariance to look smarter during job interview.

My dad being an early programmer in the 70's, I have been fortunate to switch from playing with Lego, to program my own micro-games, when I was still a kid. Since then I never stop programming.

I graduated in Mathematics and Software engineering. After a decade of C++ programming and consultancy, I got interested in the brand new .NET platform in 2002. I had the chance to write the best-seller book (in French) on .NET and C#, published by O'Reilly and also did manage some academic and professional courses on the platform and C#.

Over my consulting years I built an expertise about the architecture, the evolution and the maintenance challenges of large & complex real-world applications. It seemed like the spaghetti & entangled monolithic legacy concerned every sufficiently large team. As a consequence, I got interested in static code analysis and started the project NDepend in 2004.

Nowadays NDepend is a full-fledged Independent Software Vendor (ISV). With more than 12.000 client companies, including many of the Fortune 500 ones, NDepend offers deeper insight and full control on their application to a wide range of professional users around the world.

I live with my wife and our twin kids Léna and Paul in the beautiful island of Mauritius in the Indian Ocean.