In this article, we’re going to learn why covariance and contravariance are important, what they are, and how to use them in various scenarios using a few examples.

To download the source code for this article, you can visit our GitHub repository.

Let’s dive in.

Importance of Covariance and Contravariance in C#

When programming in C#, it’s important to know how to follow the inheritance of class definition hierarchies. For example, a GoldenRetriever class derives from a Dog class. A Dog class derives from a Animal class, and so forth.

Support Code Maze on Patreon to get rid of ads and get the best discounts on our products!
Become a patron at Patreon!

Covariance and contravariance are important concepts to know because they allow us to be flexible when dealing with class definition hierarchies and assignment compatibility. They permit implicit reference conversion for various reference types in various scenarios.

What is Covariance in C#?

Covariance in C# is a concept of preserving assignment compatibility. It allows us to assign an object, variable, or parameter of a more derived type to an object, variable, or parameter of a less derived type.

It’s important to note that covariance is a concept that can occur implicitly, without using any special keywords or syntax.

Let’s look at a simple example:

public class Person { }
public class Employee : Person { }
public class Manager : Employee { }

var personObject = new Person();
var employeeObject = new Employee();
var managerObject = new Manager();

personObject = employeeObject;
personObject = managerObject;

As we can see, we can assign the more derived Employee and Manager objects to the less derived Person object. This is the basic concept of inheritance, which allows us to substitute a Person type with an Employee or Manager type or substitute an Employee type with a Manager type in most scenarios.

What is Contravariance in C#?

Contravariance in C# is the opposite of covariance. It allows us to reverse assignment compatibility preserved by covariance.

It’s important to note that the concept of contravariance applies more to parameters instead of objects and variables. If we try to use contravariance to implicitly assign an object of a less derived class type to an object of a more derived class type, our code won’t compile:

employeeObject = personObject;  // Compile error: Cannot implicitly convert type 'Person' to 'Employee'.
managerObject = personObject;   // Compile error: Cannot implicitly convert type 'Person' to 'Manager'.

However, contravariance does allow us to assign a method with a parameter of a less derived type to a delegate with a parameter of a more derived type:

public class Person { }
public class Employee : Person { }

delegate void personDelegate(Employee employee);

static void GreetPerson(Person person)
{
   // Logic to greet person.
}

The test method:

personDelegate del = GreetPerson; 

Assert.IsTrue(del.Method.ToString() == "Void GreetPerson(Tests.Person)");

Covariance and Contravariance in Arrays

Let’s see how we can use covariance and contravariance with arrays in C#.

Covariance in Arrays

C# supports covariance for arrays, but the operation is not always type-safe. If we try to implicitly convert an array of a more derived type to an array of a less derived type, our code will compile, but an exception will occur at runtime:

public class Person { }
public class Employee : Person { }
public class Manager : Person { }

Person[] people = new Employee[5];
people[0] = new Manager();  // Runtime error: Attempted to access an element as a type incompatible with the array.

This occurs because the Employee and Manager classes derive from the Person class. If we derive the Manager class from Employee, the operation becomes type-safe. The code compiles and runs without an error since it follows the class definition hierarchy:

public class Person { }
public class Employee : Person { }
public class Manager : Employee { }

var managerObject = new Manager();
Person[] people = new Employee[5];
people[0] = managerObject;

Contravariance in Arrays

C# does not support contravariance for arrays. If we try to use contravariance with arrays, our code will not compile:

var personObject = new Person();
var employeeObject = new Employee();
var managerObject = new Manager();

Person[] people = { new Person() };
Employee[] employees = people;  // Compile error: Cannot implicitly convert type 'Person[]' to 'Employee[]'.

Covariance and Contravariance in Delegates

When working with delegates, covariance and contravariance allow us to match delegate types with method signatures.

Covariance in Delegates

Covariance allows us to use a method that has a more derived return type from the method assigned to the delegate. This gives us more flexibility with the return types of delegate methods:

public class Employee { }
public class Manager : Employee { }

static Manager GetEmployeeManager(String employeeFullName)
{
   // Logic to find employee's manager.
   return new Manager();
}

Test method:

Func<string, Manager> getManager = GetEmployeeManager; 
Func<string, Employee> getEmployee = GetEmployeeManager; 

getEmployee = getManager; 

Assert.AreEqual(getEmployee, getManager);

As we can see, covariance allows us to assign the GetEmployeeManager method to the getEmployee delegate, even though they have different return types. It also allows us to assign the getManager delegate to the getEmployee delegate. These operations are allowed since Manager derives from Employee.

Contravariance in Delegates

Contravariance in delegates applies to parameters. It allows us to assign a method with less derived parameters to a delegate that expects more derived parameters:

public class Employee { }
public class Manager : Employee { }

static void EvaluatePerformance(Employee employee)
{
   // Logic to evaluate performance.
}

Test method:

Action<Employee> evaluateEmployeePerformance = EvaluatePerformance;
Action<Manager> evaluateManagerPerformance = EvaluatePerformance;

evaluateManagerPerformance = evaluateEmployeePerformance; 

Assert.AreEqual(evaluateManagerPerformance, evaluateEmployeePerformance);

As we can see, contravariance allows us to assign the EvaluatePerformance method to the evaluateManagerPerformance delegate, even though they have different parameter types. It also allows us to assign the evaluateEmployeePerformance delegate to the evaluateManagerPerformance delegate. These operations are allowed since Manager derives from Employee.

Covariance and Contravariance in Generics

Similar to their other uses, applying covariance and contravariance to generic type parameters provides more flexibility when assigning and using types.

Covariance in Generics

We can use a covariant generic type parameter as a method return type. In C#, the out keyword specifies that a generic type parameter is covariant:

public class Person { }
public class Employee : Person { }

interface ICovariant<out T> { }
class ImplementICovariant<T> : ICovariant<T> { }

ICovariant<Person> icovPerson = new ImplementICovariant<Person>();
ICovariant<Employee> icovEmployee = new ImplementICovariant<Employee>();

icovPerson = icovEmployee;

In this example, icovEmployee can be assigned to icovPerson since ICovariant is covariant.

Contravariance in Generics

We can use contravariant generic type parameters as method parameter types. In C#, the in keyword specifies that a generic type parameter is contravariant:

public class Person { }
public class Employee : Person { }

interface IContravariant<in T> { }
class ImplementIContravariant<T> : IContravariant<T> { }
    
IContravariant<Person> icontraPerson = new ImplementIContravariant<Person>();
IContravariant<Employee> icontraEmployee = new ImplementIContravariant<Employee>();

icontraEmployee = icontraPerson;

Here, icontraPersoncan be assigned to icontraEmployee since the IContravariant interface is contravariant.

Conclusion

In this article, we learned why covariance and contravariance are important, what they are, and how they are used with arrays, delegates, and generics.

Liked it? Take a second to support Code Maze on Patreon and get the ad free reading experience!
Become a patron at Patreon!