Understanding Covariance, Contravariance, and Invariance in .NET with Fun Examples

Çağlar Can SARIKAYA
4 min readAug 14, 2023

--

The terms ‘Covariance’, ‘Contravariance’, and ‘Invariance’ might sound like the jargon of a theoretical computer science paper, but in the world of .NET, they have practical applications and can be understood with some everyday, relatable examples. Let’s dive into this topic and unravel the complexities with a sprinkle of fun!

Setting the Stage: The World of Animals

Imagine a universe filled with animals — cats, dogs, birds, and others. In our .NET world, we represent them as classes.

class Animal { }
class Cat : Animal { }
class Dog : Animal { }

Covariance

Covariance allows us to use a derived type in place of a base type.

Imagine you have a method that returns a list of Animals. Instead of returning a general animal, you want to give everyone a surprise by returning a cat.


IEnumerable<Cat> cats = new List<Cat>();
IEnumerable<Animal> animals = cats; // Covariant assignment

In real-world terms, it’s like going to an “Animal Concert” and finding out that all the performers are cats (which would be pretty cool, to be honest).

Contravariance

Contravariance allows us to use a base type in place of a derived type.

Imagine a scenario where there’s a competition for the “Best Animal Trainer.” You’re a specialized Cat trainer. Even if the competition originally calls for an Animal trainer, you can still participate because every cat is an animal.


Action<Animal> actAnimal = (Animal a) => Console.WriteLine(a.GetType().Name);
Action<Cat> actCat = actAnimal; // Contravariant assignment

In essence, you’re saying, “If you need someone to handle animals, I can do it, but remember, I specialize in cats!”

Invariance

Invariance does not allow for substitution. The type you use is the type you stick with.

Imagine a box that can hold animals. You can’t suddenly decide it’s a cat-only or dog-only box.


List<Animal> animalList = new List<Cat>(); // This will not compile.

It’s like going to an “All-Animals Ice Cream Stand” and being told you can only have cat-flavored ice cream (which sounds… unusual).

Why Does This Matter?

Understanding these concepts becomes crucial when working with generics, especially when defining or using generic interfaces and delegates. It ensures type safety, and flexibility in API design, and allows developers to prevent potential run-time errors.

Summary

Covariance, contravariance, and invariance in .NET might sound intimidating, but they can be easily remembered with our animal-themed examples:

- Covariance: “Animal Concert” with only cats.
- Contravariance: Being an “Animal Trainer” who specializes in cats.
- Invariance: The “All-Animals Ice Cream Stand” dilemma.

The next time you’re working with generics in .NET and find yourself in doubt, just remember our fun examples and the concepts will become clear as day!

When Would You Need Them in Daily Coding?

1. Interoperability with Existing Libraries: Many .NET Base Class Libraries (BCL) use covariance and contravariance in their interfaces, especially those involving collections, like `IEnumerable<T>` and `IComparer<T>`. If you interact with these libraries, understanding these concepts becomes vital.

2. API and Library Development: If you’re building an API or a library for other developers, you’ll need to consider these principles to ensure your API is both flexible and type-safe.

3. Adaptable Code: Generics make your code more reusable and adaptable. Understanding covariance and contravariance will let you write generic interfaces and delegates that can handle a wider range of scenarios without compromising on type safety.

Real-world Scenario: Processing Data with a Service

Imagine you’re building a data processing service for different types of records in a company. Let’s look at how covariance might come into play.

You have a base class `Record` and two derived classes: `EmployeeRecord` and `CustomerRecord`.


public class Record { /* common properties and methods */ }
public class EmployeeRecord : Record { /* employee-specific properties and methods */ }
public class CustomerRecord : Record { /* customer-specific properties and methods */ }

Now, you’re creating a service that processes records. Instead of creating separate methods for each record type, you want a generic approach:

 
public class RecordProcessor
{
public void ProcessRecords<T>(IEnumerable<T> records) where T : Record
{
foreach(var record in records)
{
// General processing logic
}
}
}

Here’s where covariance comes in handy:

If you have a list of `EmployeeRecord`, you can still pass it to `ProcessRecords` since `EmployeeRecord` is derived from `Record`.

var employeeRecords = new List<EmployeeRecord> { /* … */ };
var processor = new RecordProcessor();
processor.ProcessRecords(employeeRecords); // This is possible thanks to covariance

Without covariance, you’d be forced to write separate processing methods for each record type, making your code less DRY (Don’t Repeat Yourself) and harder to maintain.

In essence, understanding these variance concepts in .NET allows you to write more generic, reusable, and type-safe code, simplifying your daily tasks and interactions with existing libraries.

--

--

Çağlar Can SARIKAYA
Çağlar Can SARIKAYA

No responses yet