Covariance and Contravariance in C#

4/17/2023
3 minute read

Let's talk about Contravariance and Covariance in C# using .NET Framework examples!

Contravariance and covariance are essential concepts in C# when dealing with generics, enabling us to have more flexibility when assigning generic types. So let's have examples straight from the framework itself!

Also, we will go a bit deeper and talk what some differences between generic constraints and things like Contravariance are.

Covariance

Allows a method to return a more derived type than specified by the generic type. In other words, you can use a derived class where a base class is expected.

Example: The IEnumerable<out T> interface. IEnumerable<T> is covariant, meaning that if Dog is a subclass of Animal, you can use IEnumerable<Dog> where IEnumerable<Animal> is expected.

IEnumerable<Dog> dogs = new List<Dog> { new Dog(), new Dog() };
IEnumerable<Animal> animals = dogs; // Covariant assignment

Contravariance

Enables you to use a more generic (less derived) type than specified by the generic type. In other words, you can use a base class where a derived class is expected.

Example: The IComparer<in T> interface. IComparer<T> is contravariant, meaning that if Animal is a base class of Dog, you can use IComparer<Animal> where IComparer<Dog> is expected.

public class AnimalComparer : IComparer<Animal>
{
    public int Compare(Animal x, Animal y) { /*...*/ }
}

IComparer<Dog> dogComparer = new AnimalComparer(); // Contravariant assignment

You might ask: What code does it make easier to use contravariance? Why not use generics in this case or directly the base type?

Contravariance simplifies code by allowing you to create more reusable and maintainable components, especially when working with delegates and interfaces. While generics alone provide type safety, contravariance offers an additional layer of flexibility.

Consider the following example using the base type directly:

public class Animal { }
public class Dog : Animal { }

public class AnimalHandler
{
    public void Handle(Animal animal) { /*...*/ }
}

public class DogHandler : AnimalHandler
{
    public void Handle(Dog dog) { /*...*/ }
}

Now imagine we have a method that takes a list of dogs and an AnimalHandler:

public void ProcessDogs(List<Dog> dogs, AnimalHandler handler)
{
    foreach (var dog in dogs)
    {
        handler.Handle(dog);
    }
}

This works fine for an AnimalHandler instance, but if you want to use a DogHandler, you'll have to modify the ProcessDogs method. This creates tight coupling and reduces reusability.

Now let's use contravariance with an interface:

public interface IHandler<in T>
{
    void Handle(T item);
}

public class AnimalHandler : IHandler<Animal>
{
    public void Handle(Animal animal) { /*...*/ }
}

public class DogHandler : IHandler<Dog>
{
    public void Handle(Dog dog) { /*...*/ }
}

With contravariance, the ProcessDogs method can accept an IHandler<Dog> instead:

public void ProcessDogs(List<Dog> dogs, IHandler<Dog> handler)
{
    foreach (var dog in dogs)
    {
        handler.Handle(dog);
    }
}

Now, you can pass either an AnimalHandler or a DogHandler to ProcessDogs, as both are compatible with IHandler<Dog>. This promotes loose coupling, making the code more reusable and maintainable.

If I define my IHandler like this:

public interface IHandler<TAnimal> where TAnimal : Animal

Wouldn't that achieve the same?

Yes, using a constraint on the generic type achieves a similar goal. However, it's essential to understand the differences between using constraints and using contravariance to choose the best approach for your needs.

By defining IHandler<TAnimal> with a constraint:

public interface IHandler<TAnimal> where TAnimal : Animal
{
    void Handle(TAnimal item);
}

You can implement specialized handlers for each derived type:

public class AnimalHandler : IHandler<Animal>
{
    public void Handle(Animal animal) { /*...*/ }
}

public class DogHandler : IHandler<Dog>
{
    public void Handle(Dog dog) { /*...*/ }
}

However, the ProcessDogs method will have to be defined as:

public void ProcessDogs<TAnimal>(List<TAnimal> animals, IHandler<TAnimal> handler) where TAnimal : Animal
{
    foreach (var animal in animals)
    {
        handler.Handle(animal);
    }
}

You can still call ProcessDogs with either an AnimalHandler or a DogHandler:

ProcessDogs(new List<Dog> { new Dog(), new Dog() }, new AnimalHandler());
ProcessDogs(new List<Dog> { new Dog(), new Dog() }, new DogHandler());

The primary difference is that the ProcessDogs method is now generic, and the type safety and flexibility come from the generic type constraint. In contrast, contravariance provides flexibility directly in the interface definition without the need for a generic method.

Conclusion

Covariance and contravariance help you to write more flexible and reusable code!

Mutable value types are evil! Sort of...

You might have heard that mutable value types are evil. But why is that and why does the .NET framework use them then? Are they really that evil?

Let's have a look at a few examples and have a look what is going on!

Caching in .NET with MemoryCache

In this blog post, we will discuss how we can "cache" entries from the database. We will talk about why we would do this in the first place and how to achieve that.

Also, we will talk about some implications and what "cache invalidation" is.

static abstract interfaces and generic math

Besides the big announcements of .NET 6 there are also some smaller features. I want to show case a special one: static abstract interfaces. With this you have ability to extend the contract in that sense, that an implementing class has to provide also static methods. This feature is right now flagged as preview, but you can use it if you want.

This also enables generic math operations on an interface level.

An error has occurred. This application may no longer respond until reloaded. Reload x