Course contentsShow
← C#
Lesson 128 of 1,9082. Object-Oriented Design in C#Free lesson

Designing for Invariance, Covariance, and Contravariance

You will apply variance rules to method signatures and delegates to achieve flexible yet type-safe hierarchies.

In this lesson, you will learn how to design method signatures and delegates that allow for flexible object substitution while maintaining strict type safety.

The Problem: Strict Types vs. Flexible Logic

Usually, C# is invariant. If you have a method that expects a List<Animal>, you cannot pass it a List<Dog>, even though a Dog is an Animal. This prevents you from accidentally putting a Cat into a list intended only for Dogs. However, when we design APIs, we often need more flexibility.

Covariance: Returning More Specific Types

Covariance allows you to use a more specific type than originally specified. Imagine a delegate that acts as a factory. If a method signature promises to return an Animal, you can safely provide a method that returns a Dog. Because a Dog is an Animal, the caller is never surprised.

// Covariance: Returning a Dog where an Animal is expected
delegate Animal AnimalFactory();

Dog GetDog() => new Dog();

AnimalFactory factory = GetDog; // This works!

Contravariance: Accepting More General Types

Contravariance is the inverse: it allows you to use a more general type than originally specified. If you have a delegate that defines an action for a Dog, you can assign a method that handles any Animal. Why? Because if the method knows how to handle any Animal, it is perfectly capable of handling a Dog.

// Contravariance: Handling an Animal where a Dog is expected
delegate void DogHandler(Dog dog);

void HandleAnyAnimal(Animal animal) { /* ... */ }

DogHandler handler = HandleAnyAnimal; // This also works!

Designing for Variance

When designing your systems, remember this rule of thumb:

  1. Use Covariance for "out" positions (return types). You can always return something more specific than what was requested.
  2. Use Contravariance for "in" positions (input parameters). You can always accept something more general than what was requested.

By applying these patterns to your delegates and method signatures, you create robust interfaces that remain type-safe without forcing your users to write unnecessary casting code.

Key Takeaway: Covariance allows for more specific return types, while Contravariance allows for more general input parameters, providing the flexibility to substitute types safely.