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,
1 2 3 |
public class Account{ } public class CheckingAccount: Account{ } public class SavingAccount: Account{ } |
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:
1 2 3 4 5 6 |
public class Base { public Account GetAccount() { ... } } public class Derived { public CheckingAccount GetAccount() { ... } } |
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.
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:
1 2 3 |
IEnumerable<CheckingAccount> derivedAccounts = new List<CheckingAccount>(); IEnumerable<Account> accounts= derivedAccounts; // There is no need to use Cast<T>() like in derivedAccounts.Cast<Account>() |
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.
1 2 3 4 |
if (x is IEnumerable<Account>) Method1(); else if (x is IEnumerable<CheckingAccount>) Method2(); |
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.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
class Program { static void Main() { IComparer<Account> comparer1 = new AccountComparer(); IComparer<CheckingAccount> comparer2 = comparer1; if (comparer2.Compare(new CheckingAccount("Lena"), new CheckingAccount("Paul")) == 0) { Console.WriteLine("Both checking accounts belongs to same Person!"); } } } class AccountComparer : IComparer<Account> { public int Compare(Account x, Account y) { return string.CompareOrdinal(x.SSN, y.SSN); } } |
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:
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:
1 2 3 4 5 6 7 8 |
public delegate Account HandlerMethod(); public static Account AccountHandler() { return null; } public static CheckingAccount AccountCheckinghHandler() { return null; } static void CovarianceTest() { HandlerMethod h1 = AccountHandler; // Covariance makes the below assignment possible HandlerMethod h2 = AccountCheckinghHandler; } |
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:
1 |
public delegate TResult Func<in T,out TResult>(T arg); |
- 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.
1 2 3 |
Action<Account> action1 = (target) => { Console.WriteLine(target.GetType().Name); }; Action<CheckingAccount> action2 = action1; action2(new CheckingAccount("")); |
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.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
public delegate void KeyEventHandler(object sender, KeyEventArgs e); public delegate void MouseEventHandler(object sender, MouseEventArgs e); public partial class MyForm : Form { // Event handler that accepts a parameter of the EventArgs type. private void MultiHandler(object sender, System.EventArgs e) { label1.Text = System.DateTime.Now.ToString(); } public MyForm() { InitializeComponent(); // The keydown event is of type KeyEventHandler this.button1.KeyDown += this.MultiHandler; // The MouseClick event is of type MouseEventHandler this.button1.MouseClick += this.MultiHandler; } } |
Finally here is a delegate Func<TResult> covariance example. func2 can return any Account and thus it can return any CheckingAccount!
1 2 3 |
Func<CheckingAccount> func1 = () => new CheckingAccount("Lena"); Func<Account> func2 = func1; Account account = func2(); |
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:
1 2 3 4 5 |
CheckingAccount[] array1 = { new CheckingAccount() }; // Here the covariance relationship implies that an array of CheckingAccount is an array of Account. Account[] array2 = array1; // But this is not safe!! array2[0] = new SavingAccount(); // BOUM! ArrayTypeMistmatchException thrown by the runtime!! |
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:
1 2 3 4 5 |
CheckingAccount[] array1 = { new CheckingAccount() }; IEnumerable<Account> seq2 = array1; // This is safe!! // Invalid assignment, IEnumerable is read-only in the sense that its elements cannot be overridden //seq2[0] = new SavingAccount(); |
Contravariance in Array
C# doesn’t support implicit contravariance on array. This code doesn’t compile:
An explicit cast is supported by the compiler but it leads to an InvalidCastException at runtime!
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 😊.