Covariance & Contravariance in C#: Make Variance Work for You

Type Compatibility Beyond Exact Matches

If you've ever tried to pass an IEnumerable<Dog> to a method expecting IEnumerable<Animal> and it just worked, you've benefited from covariance. Without variance, generics would be rigid—you'd need exact type matches even when the conversion is logically safe. This inflexibility would force you to write redundant code or lose type safety through casting.

Variance lets the type system relax strict generic matching when safe to do so. Covariance allows using more derived types (Dog where Animal is expected), while contravariance allows less derived types (Animal where Dog is expected). The in and out keywords control these conversions at compile time, preventing runtime type errors.

You'll learn how to use out for covariant interfaces and delegates, apply in for contravariant parameters, understand why List<T> can't be variant while IEnumerable<T> can, and avoid common pitfalls that break type safety. You'll see practical examples with collections, delegates, and custom interfaces.

Covariance with the out Keyword

Covariance lets you treat a generic type with a derived parameter as if it has a base parameter. You mark type parameters with out to indicate they only appear in output positions (return values), never input positions (method parameters). This guarantees type safety since the interface or delegate only produces values, never consumes them.

IEnumerable<T> is the classic example. Since it only returns T values through its iterator and never accepts T as input, it's safely covariant. This lets you use a more specific collection where a general one is expected.

Covariance.cs
// Base and derived classes
public class Animal
{
    public string Name { get; set; } = string.Empty;
    public virtual void Speak() => Console.WriteLine("Some sound");
}

public class Dog : Animal
{
    public override void Speak() => Console.WriteLine("Woof!");
}

public class Cat : Animal
{
    public override void Speak() => Console.WriteLine("Meow!");
}

// Covariant interface using 'out' keyword
public interface IAnimalRepository<out T> where T : Animal
{
    T GetById(int id);  // T only in return position - safe
    IEnumerable<T> GetAll();  // T only in return position - safe
    // void Add(T animal);  // Would NOT compile - T in input position
}

public class DogRepository : IAnimalRepository<Dog>
{
    private readonly List<Dog> _dogs = new()
    {
        new Dog { Name = "Buddy" },
        new Dog { Name = "Max" }
    };

    public Dog GetById(int id) => _dogs[id];
    public IEnumerable<Dog> GetAll() => _dogs;
}

// Usage demonstrating covariance
public class AnimalService
{
    public void ProcessAnimals(IAnimalRepository<Animal> repository)
    {
        var animals = repository.GetAll();
        foreach (var animal in animals)
        {
            animal.Speak();
        }
    }
}

// This works due to covariance!
var dogRepo = new DogRepository();
var service = new AnimalService();
service.ProcessAnimals(dogRepo);  // IAnimalRepository<Dog> → IAnimalRepository<Animal>

The out keyword allows treating IAnimalRepository<Dog> as IAnimalRepository<Animal> since GetById and GetAll only return animals, never accept them as parameters. If we added an Add(T animal) method, the interface could no longer use out because T would appear in an input position.

Contravariance with the in Keyword

Contravariance is the reverse of covariance. It allows using a less specific type where a more specific one is expected. You mark type parameters with in to indicate they only appear in input positions (method parameters), never output positions (return types). This is common with delegates and handlers that process values.

Contravariance.cs
// Contravariant interface using 'in' keyword
public interface IAnimalHandler<in T> where T : Animal
{
    void Handle(T animal);  // T only in input position - safe
    void Process(T animal, string action);  // T only in input - safe
    // T GetAnimal();  // Would NOT compile - T in output position
}

public class AnimalHandler : IAnimalHandler<Animal>
{
    public void Handle(Animal animal)
    {
        Console.WriteLine($"Handling animal: {animal.Name}");
        animal.Speak();
    }

    public void Process(Animal animal, string action)
    {
        Console.WriteLine($"Processing {animal.Name}: {action}");
    }
}

// Usage demonstrating contravariance
public class PetClinic
{
    public void TreatDog(Dog dog, IAnimalHandler<Dog> handler)
    {
        Console.WriteLine($"Treating dog: {dog.Name}");
        handler.Handle(dog);
    }
}

// This works due to contravariance!
var clinic = new PetClinic();
var generalHandler = new AnimalHandler();

var myDog = new Dog { Name = "Rex" };
clinic.TreatDog(myDog, generalHandler);  // IAnimalHandler<Animal> → IAnimalHandler<Dog>

The in keyword lets you use IAnimalHandler<Animal> where IAnimalHandler<Dog> is expected. This is safe because a handler that can process any Animal can certainly handle a Dog. The handler only consumes animals, never produces them, making contravariance type-safe.

Variance in Delegates

Built-in generic delegates like Func<T> and Action<T> have variance annotations. Func<out TResult> is covariant in its return type, while Action<in T> is contravariant in its parameter type. This enables natural conversions when working with lambda expressions and method groups.

DelegateVariance.cs
public class VarianceDemo
{
    // Method that returns Dog
    public Dog CreateDog() => new Dog { Name = "Spot" };

    // Method that accepts Animal parameter
    public void FeedAnimal(Animal animal)
    {
        Console.WriteLine($"Feeding {animal.Name}");
    }

    public void DemonstrateVariance()
    {
        // Covariance: Func<Dog> → Func<Animal>
        Func<Dog> dogFactory = CreateDog;
        Func<Animal> animalFactory = dogFactory;  // Covariant conversion
        Animal animal = animalFactory();
        Console.WriteLine($"Created: {animal.Name}");

        // Contravariance: Action<Animal> → Action<Dog>
        Action<Animal> animalFeeder = FeedAnimal;
        Action<Dog> dogFeeder = animalFeeder;  // Contravariant conversion
        dogFeeder(new Dog { Name = "Buddy" });
    }
}

// Practical example: event handlers
public class EventVariance
{
    public event Action<Animal>? AnimalEvent;

    public void Subscribe()
    {
        // Can subscribe with more specific handler due to contravariance
        Action<object> generalHandler = obj =>
            Console.WriteLine($"Handled: {obj}");

        AnimalEvent += generalHandler;  // Action<object> → Action<Animal>
    }

    public void RaiseEvent(Animal animal)
    {
        AnimalEvent?.Invoke(animal);
    }
}

Delegate variance makes event handling and LINQ operations more flexible. You can assign methods with compatible but not identical signatures, letting the compiler handle the safe conversions automatically.

Creating Variant Generic Interfaces

When designing your own generic interfaces, consider adding variance annotations to improve usability. The key is ensuring type parameters only appear in appropriate positions—outputs only for covariance, inputs only for contravariance.

CustomVariant.cs
// Covariant producer interface
public interface IProducer<out T>
{
    T Produce();
    IEnumerable<T> ProduceMany(int count);
}

// Contravariant consumer interface
public interface IConsumer<in T>
{
    void Consume(T item);
    void ConsumeMany(IEnumerable<T> items);
}

// Invariant interface (has both input and output positions)
public interface IRepository<T>
{
    T GetById(int id);    // Output position
    void Add(T entity);   // Input position
    // Cannot use 'in' or 'out' - T used in both positions
}

// Implementation
public class DogProducer : IProducer<Dog>
{
    public Dog Produce() => new Dog { Name = "New Dog" };

    public IEnumerable<Dog> ProduceMany(int count)
    {
        for (int i = 0; i < count; i++)
            yield return new Dog { Name = $"Dog {i}" };
    }
}

public class AnimalConsumer : IConsumer<Animal>
{
    public void Consume(Animal item)
    {
        Console.WriteLine($"Consumed: {item.Name}");
    }

    public void ConsumeMany(IEnumerable<Animal> items)
    {
        foreach (var item in items)
            Consume(item);
    }
}

// Using variance
IProducer<Dog> dogProducer = new DogProducer();
IProducer<Animal> animalProducer = dogProducer;  // Covariance

IConsumer<Animal> animalConsumer = new AnimalConsumer();
IConsumer<Dog> dogConsumer = animalConsumer;  // Contravariance

The producer/consumer pattern clearly shows when variance applies. Producers only output values (covariant), consumers only input values (contravariant), while repositories doing both remain invariant. Design your interfaces with single responsibilities to enable variance where possible.

Avoiding Common Mistakes

The most frequent error is trying to use variance with classes. Only interfaces and delegates support variance in C#. If you need variance with a concrete type, extract an interface with appropriate annotations. Another mistake is attempting variance when the type parameter appears in both input and output positions—this requires invariance.

Array covariance is a legacy feature that's runtime-checked, not compile-time checked. Never rely on it for new code since it can throw ArrayTypeMismatchException at runtime. Always use generic collections like List<T> or IEnumerable<T> which provide compile-time type safety.

Don't confuse variance with conversion. Variance doesn't change the underlying type—an IEnumerable<Dog> remains a sequence of dogs even when treated as IEnumerable<Animal>. You're changing the compile-time view of the type, not performing a conversion. This distinction matters for performance and understanding what operations remain available.

Try It Yourself

Build a console app demonstrating covariance and contravariance with custom interfaces and delegates.

Steps

  1. Create: dotnet new console -n VarianceDemo
  2. Navigate: cd VarianceDemo
  3. Update Program.cs with code below
  4. Run: dotnet run
VarianceDemo.csproj
<Project Sdk="Microsoft.NET.Sdk">
  <PropertyGroup>
    <OutputType>Exe</OutputType>
    <TargetFramework>net8.0</TargetFramework>
    <Nullable>enable</Nullable>
    <ImplicitUsings>enable</ImplicitUsings>
  </PropertyGroup>
</Project>
Program.cs
Console.WriteLine("=== Variance Demo ===\n");

// Covariance example
IEnumerable<Dog> dogs = new List<Dog>
{
    new() { Name = "Rex" },
    new() { Name = "Buddy" }
};

IEnumerable<Animal> animals = dogs;  // Covariant conversion
Console.WriteLine("Covariance - Dogs as Animals:");
foreach (var animal in animals)
    Console.WriteLine($"  {animal.Name}");

// Contravariance example
Action<Animal> animalHandler = a => Console.WriteLine($"Handling: {a.Name}");
Action<Dog> dogHandler = animalHandler;  // Contravariant conversion

Console.WriteLine("\nContravariance - Animal handler for Dogs:");
dogHandler(new Dog { Name = "Max" });

public class Animal
{
    public string Name { get; set; } = string.Empty;
}

public class Dog : Animal { }

What you'll see

=== Variance Demo ===

Covariance - Dogs as Animals:
  Rex
  Buddy

Contravariance - Animal handler for Dogs:
Handling: Max

Quick FAQ

When should I use 'out' for covariance?

Use out when your generic parameter only appears in return types, never in input parameters. This allows treating IEnumerable<Derived> as IEnumerable<Base>. IEnumerable<T>, IQueryable<T>, and Func<T> delegates use covariance since they only produce values, never consume them.

What's the difference between covariance and contravariance?

Covariance (out) lets you use a more derived type than originally specified—treating IEnumerable<Dog> as IEnumerable<Animal>. Contravariance (in) allows less derived types—using Action<Animal> where Action<Dog> is expected. Covariance is for outputs, contravariance for inputs.

Why can't List<T> use variance like IEnumerable<T>?

List<T> has Add and other methods that accept T as input, making T appear in both input and output positions. Variance requires T to appear only in output (covariance) or only in input (contravariance) positions. IEnumerable<T> works because it's read-only with no input methods.

Is variance checked at compile time or runtime?

Variance is compile-time checked for interfaces and delegates. The compiler ensures type safety based on in/out annotations. Arrays have runtime-checked covariance in C# for backward compatibility, which can throw ArrayTypeMismatchException. Always prefer generic collections over arrays for type safety.

Can I use variance with custom generic classes?

No, only interfaces and delegates support variance in C#. Classes cannot use in/out modifiers even if they meet variance rules. If you need variance with classes, define an interface with the appropriate variance annotations and have your class implement it.

Back to Articles