0
0
CsharpConceptBeginner · 4 min read

Covariance and Contravariance in C#: Explained Simply

In C#, covariance allows a method to return a more derived type than originally specified, while contravariance allows a method to accept parameters of less derived types. These concepts help make generic interfaces and delegates more flexible and safe when working with inheritance.
⚙️

How It Works

Imagine you have a family tree where a Dog is a type of Animal. Covariance lets you use a more specific type (like Dog) when a more general type (like Animal) is expected. It's like saying, "I need an animal, but giving a dog is okay because a dog is an animal." This usually applies to output or return types.

Contravariance is the opposite. It lets you use a more general type where a more specific type is expected. For example, if a method expects a Animal, contravariance allows you to pass a Dog instead. This works well for input parameters, like method arguments.

In C#, covariance and contravariance mainly apply to generic interfaces and delegates, making your code more flexible without losing type safety.

💻

Example

This example shows covariance with IEnumerable<out T> and contravariance with IComparer<in T>. Notice how you can assign a collection of Dog to a variable expecting Animal (covariance), and a comparer for Animal to a variable expecting a comparer for Dog (contravariance).

csharp
using System;
using System.Collections.Generic;

class Animal { public string Name => "Animal"; }
class Dog : Animal { public string Breed => "Beagle"; }

class Program
{
    static void Main()
    {
        IEnumerable<Dog> dogs = new List<Dog> { new Dog() };
        // Covariance: IEnumerable<Dog> can be assigned to IEnumerable<Animal>
        IEnumerable<Animal> animals = dogs;
        foreach (var animal in animals)
        {
            Console.WriteLine(animal.Name);
        }

        IComparer<Animal> animalComparer = new AnimalComparer();
        // Contravariance: IComparer<Animal> can be assigned to IComparer<Dog>
        IComparer<Dog> dogComparer = animalComparer;
        int result = dogComparer.Compare(new Dog(), new Dog());
        Console.WriteLine($"Comparison result: {result}");
    }
}

class AnimalComparer : IComparer<Animal>
{
    public int Compare(Animal x, Animal y) => 0; // simple comparer
}
Output
Animal Comparison result: 0
🎯

When to Use

Use covariance when you want to return more specific types from methods or properties, especially with read-only collections like IEnumerable<T>. It helps when you want to treat a collection of derived types as a collection of base types.

Use contravariance when you want to accept more general types as input parameters, such as in delegates or interfaces like IComparer<T>. This allows you to write more flexible code that can handle a wider range of inputs.

Real-world examples include event handlers, sorting algorithms, and APIs that work with collections of objects where inheritance is involved.

Key Points

  • Covariance applies to output types and allows using a more derived type.
  • Contravariance applies to input types and allows using a less derived type.
  • They improve flexibility and type safety in generic interfaces and delegates.
  • Covariance uses the out keyword; contravariance uses the in keyword in C#.
  • Common interfaces supporting these are IEnumerable<out T> (covariant) and IComparer<in T> (contravariant).

Key Takeaways

Covariance allows using a more specific type for output (return values).
Contravariance allows using a more general type for input (method parameters).
Use out for covariance and in for contravariance in generic interfaces.
They make your code more flexible and safe when working with inheritance.
Common examples include IEnumerable<T> for covariance and IComparer<T> for contravariance.