NDepend Blog

Improve your .NET code quality with NDepend

Covariance and Contravariance in C# Explained

May 2, 2024 6 minutes read

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#. These concepts will be first introduced for generics, then for delegates, and finally for arrays.

Review of Sub-Typing in Object-Oriented Programming (OOP)

Before going further, let’s briefly remind some basic OOP notions. 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. In other words, an expression of type CheckingAccount can be used wherever an expression of type Account is used. 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 return a SavingAccount or a CheckingAccount reference, the code below is valid. This is a wonderful new feature of C#9 named Covariant Return Type:

It is All About Implicit Reference Conversion

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 the out parameters.

Covariance in Generics

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

It 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 motivators was to remove the performance costly 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 for a specific generic type is implemented using the in generic modifier. It applies to generic parameter types used as method parameters. 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 a delegate contravariant parameter. The MultiHandler() method can accept as a parameter any KeyEventArgs object. Thus it can accept 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 code sample below). This is because IEnumerable<out T> is read-only, there is no in parameter. Therefore, updating an element in the sequence poses no risk:

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 arrays 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 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, my advice is to continue using scientific terms like Covariance and Contravariance to look smarter during job interviews 😊.

Leave a Reply

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