Covariance and Contravariance in C#

17/04/2023
C#.NET

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!

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