DEV Community

Fabrizio Bagalà
Fabrizio Bagalà

Posted on

Covariance and Contravariance in C#

The world of object-oriented programming is filled with intricate concepts, among which covariance and contravariance hold significant roles. These principles define the permissible type relationships in programming languages, especially when dealing with generic types.

Covariance

Covariance allows the use of a derived type in place of a base type. It is supported for reference type parameters in method returns, arrays, delegates, and generic interfaces.

Consider the following example with a base class Animal and a derived class Dog. If we have a method that returns an Animal, we can have it return a Dog without problems, since Dog is a derivative of Animal.

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

public Animal GetAnimal()
{
    return new Dog(); // This operation is allowed
}
Enter fullscreen mode Exit fullscreen mode

However, you can not do the opposite, i.e., have it return an Animal when you expect a Dog:

public Dog GetDog()
{
    return new Animal(); // This operation is NOT allowed
}
Enter fullscreen mode Exit fullscreen mode

Regarding generic types, covariance is only allowed for read operations on reference types:

IEnumerable<Dog> dogs = new List<Dog>();
IEnumerable<Animal> animals = dogs; // This operation is allowed
Enter fullscreen mode Exit fullscreen mode

However, it is not possible to use an IEnumerable<Animal> where an IEnumerable<Dog> is expected:

IEnumerable<Animal> animals = new List<Animal>();
IEnumerable<Dog> dogs = animals; // This operation is NOT allowed
Enter fullscreen mode Exit fullscreen mode

Contravariance

Contravariance is the concept opposite to covariance. It allows the use of a base type in place of a derived type. Contravariance is supported for reference type parameters in method parameter positions, delegates, and generic interfaces.

Consider the following example with a delegate:

delegate void Action<in T>(T arg);

static void Main()
{
    Action<Animal> animalAction = (Animal a) => Console.WriteLine(a);
    Action<Dog> dogAction = animalAction; // This operation is allowed
    dogAction(new Dog()); // Prints "Dog"
}
Enter fullscreen mode Exit fullscreen mode

In this example, the delegate accepts an Animal type. Since Dog is a derivative of Animal, we can assign animalAction to dogAction. However, we can't do the opposite:

Action<Dog> dogAction = (Dog d) => Console.WriteLine(d);
Action<Animal> animalAction = dogAction; // This operation is NOT allowed
Enter fullscreen mode Exit fullscreen mode

In summary, contravariance allows only write operations on reference types.

Invariance

If a type is neither covariant nor contravariant, it is said to be invariant. An invariant generic type maintains the exact identity of the type parameter. This means you cannot use a different type than the one specified as the parameter of the generic type.

For instance, in the generic class List<T>, T is invariant. This means you can not use a List<Animal> where you expect a List<Dog>, or vice versa, even though Dog is a derived type of Animal.

List<Animal> animals = new List<Dog>(); // This operation is NOT allowed
Enter fullscreen mode Exit fullscreen mode

Similarly, if you have a method that accepts a List<Animal>, you can not pass a List<Dog>:

void ProcessAnimals(List<Animal> animals) { /*...*/ }

List<Dog> dogs = new List<Dog>();
ProcessAnimals(dogs); // This operation is NOT allowed
Enter fullscreen mode Exit fullscreen mode

Although invariance might seem limiting, it is essential for type safety. Without invariance, you could introduce type errors into your code, as in the following example:

List<Animal> animals = new List<Dog>(); // Suppose this operation was allowed
animals.Add(new Cat()); // This would be an error because we're trying to add a Cat to a list of Dogs
Enter fullscreen mode Exit fullscreen mode

Invariance ensures that these situations do not occur, preserving the integrity and safety of your code.

Conclusion

Covariance and contravariance might sound complicated, but they are really helpful in making your code flexible. C# strongly supports these ideas, and you can use them in many situations. But you need to be careful, as using them without fully understanding them can cause confusing errors. So, it is important to really understand what covariance and contravariance mean before you start using them to boost your code's potential.

References

Top comments (0)