You are viewing a preview of this lesson. Sign in to start learning
Back to C# Programming

Covariance & Contravariance

Understand in/out modifiers for safe variance in generic interfaces and delegates

Covariance & Contravariance

Master covariance and contravariance in C# with free flashcards and spaced repetition practice. This lesson covers generic variance annotations, safe type conversions, and practical applications in interfaces and delegatesβ€”essential concepts for writing flexible, type-safe code with C#'s advanced generic system.

Welcome to Variance in C# Generics πŸ’»

Imagine you have a box labeled "Animal Container." Intuitively, you'd expect to be able to treat a box of cats as a box of animals when you're only reading from it. But what about when you're writing to it? These intuitions about type compatibility form the foundation of varianceβ€”one of the most powerful yet misunderstood features in C#'s type system.

Variance allows you to use a more derived type (specific) where a base type (general) is expected, or vice versa, in certain controlled scenarios. Understanding variance is crucial for writing elegant, reusable code with interfaces and delegates, especially when working with LINQ, collections, and event handling.

Core Concepts: Understanding Variance 🎯

What Is Variance?

Variance describes how subtyping relationships between types relate to subtyping relationships between more complex types based on them. In simpler terms: if Cat is a subtype of Animal, is IEnumerable<Cat> a subtype of IEnumerable<Animal>?

There are three kinds of variance:

Type Symbol Conversion Direction Safety Rule
Covariance out More specific β†’ More general Output positions only
Contravariance in More general β†’ More specific Input positions only
Invariance (none) No conversion allowed Both input and output

Covariance: The "Out" Direction πŸ”Ί

Covariance allows you to use a more derived type than originally specified. It preserves the direction of inheritance: if Derived extends Base, then ICovariant<Derived> can be used as ICovariant<Base>.

The key insight: covariance is safe when you're only producing (outputting) values, never consuming (inputting) them.

public interface IProducer<out T>
{
    T Produce();  // βœ… OK: T is in output position
    // void Consume(T item);  // ❌ Would be compiler error
}

Why "out"? The type parameter is only used in output positions (return values).

πŸ’‘ Memory Aid: OUTput β†’ COvariance (both start with vowels!)

Contravariance: The "In" Direction πŸ”»

Contravariance allows you to use a more generic (less derived) type than originally specified. It reverses the direction of inheritance: if Derived extends Base, then IContravariant<Base> can be used as IContravariant<Derived>.

The key insight: contravariance is safe when you're only consuming (inputting) values, never producing (outputting) them.

public interface IConsumer<in T>
{
    void Consume(T item);  // βœ… OK: T is in input position
    // T Produce();  // ❌ Would be compiler error
}

Why "in"? The type parameter is only used in input positions (method parameters).

πŸ’‘ Memory Aid: INput β†’ CONTRAvariance (both have 'N's!)

Invariance: No Conversion Allowed πŸ”’

When a type parameter is used in both input and output positions, it must be invariantβ€”no variance annotations allowed.

public interface IStorage<T>  // No 'in' or 'out'
{
    void Store(T item);    // Input position
    T Retrieve();          // Output position
}

Invariance prevents type safety violations. IStorage<Cat> cannot be converted to IStorage<Animal> because if it could, you might store a Dog where a Cat is expected!

Real-World Examples 🌍

Example 1: Covariance with IEnumerable

The most common example of covariance is IEnumerable<T>, which is defined as:

public interface IEnumerable<out T>
{
    IEnumerator<T> GetEnumerator();
}

This allows natural conversions when working with collections:

public class Animal 
{ 
    public string Name { get; set; } 
}

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

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

// Usage:
IEnumerable<Cat> cats = new List<Cat> 
{ 
    new Cat { Name = "Whiskers" },
    new Cat { Name = "Mittens" }
};

// βœ… Covariance allows this conversion:
IEnumerable<Animal> animals = cats;

foreach (Animal animal in animals)
{
    Console.WriteLine(animal.Name);  // Works perfectly
}

Why is this safe? You can only read from IEnumerable<T>. You can't add items to it. So treating cats as animals when reading is perfectly safeβ€”a cat is an animal.

What wouldn't be safe:

// If IList<T> were covariant (it's not!):
IList<Cat> cats = new List<Cat>();
IList<Animal> animals = cats;  // ❌ Compiler error!
// animals.Add(new Dog());  // Would add a Dog to a List<Cat>!

Example 2: Contravariance with IComparer

The IComparer<T> interface demonstrates contravariance:

public interface IComparer<in T>
{
    int Compare(T x, T y);
}

This enables elegant reuse of comparison logic:

public class Animal
{
    public string Name { get; set; }
    public int Age { get; set; }
}

public class Cat : Animal
{
    public string Favoritetoy { get; set; }
}

// Comparer that works for any Animal:
public class AnimalAgeComparer : IComparer<Animal>
{
    public int Compare(Animal x, Animal y)
    {
        return x.Age.CompareTo(y.Age);
    }
}

// Usage:
List<Cat> cats = new List<Cat>
{
    new Cat { Name = "Whiskers", Age = 3 },
    new Cat { Name = "Mittens", Age = 1 },
    new Cat { Name = "Shadow", Age = 5 }
};

IComparer<Animal> animalComparer = new AnimalAgeComparer();

// βœ… Contravariance allows using Animal comparer for Cats:
IComparer<Cat> catComparer = animalComparer;
cats.Sort(catComparer);

foreach (var cat in cats)
{
    Console.WriteLine($"{cat.Name}: {cat.Age} years old");
}
// Output:
// Mittens: 1 years old
// Whiskers: 3 years old
// Shadow: 5 years old

Why is this safe? The comparer only receives values (input). If it can compare any Animal, it can certainly compare the more specific Cat objects. A function that handles the general case automatically handles the specific case.

Example 3: Variance in Delegates 🎭

Delegates support both covariance (for return types) and contravariance (for parameters):

public delegate T Factory<out T>();
public delegate void Action<in T>(T obj);

Covariant return types:

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

// Method that returns a Cat:
public Cat CreateCat()
{
    return new Cat();
}

// βœ… Can assign to delegate expecting Animal:
Factory<Animal> animalFactory = CreateCat;
Animal animal = animalFactory();  // Returns a Cat, which is an Animal

Contravariant parameters:

public void FeedAnimal(Animal animal)
{
    Console.WriteLine("Feeding animal...");
}

// βœ… Can assign to delegate expecting Cat:
Action<Cat> catFeeder = FeedAnimal;
catFeeder(new Cat());  // Calls FeedAnimal with a Cat

Example 4: Building a Flexible Repository Pattern πŸ›οΈ

Variance shines in real-world architectural patterns:

public interface IReadOnlyRepository<out T>
{
    IEnumerable<T> GetAll();
    T GetById(int id);
}

public interface IWriteOnlyRepository<in T>
{
    void Add(T item);
    void Update(T item);
    void Delete(T item);
}

// Domain models:
public class Entity { public int Id { get; set; } }
public class User : Entity { public string Username { get; set; } }
public class AdminUser : User { public string[] Permissions { get; set; } }

// Implementation:
public class UserRepository : IReadOnlyRepository<User>
{
    private List<User> _users = new();
    
    public IEnumerable<User> GetAll() => _users;
    public User GetById(int id) => _users.FirstOrDefault(u => u.Id == id);
}

// Usage with covariance:
IReadOnlyRepository<User> userRepo = new UserRepository();
// βœ… Can treat as Entity repository when only reading:
IReadOnlyRepository<Entity> entityRepo = userRepo;

// Contravariant writer:
public class EntityAuditor : IWriteOnlyRepository<Entity>
{
    public void Add(Entity item) 
        => Console.WriteLine($"Auditing new entity: {item.Id}");
    public void Update(Entity item) 
        => Console.WriteLine($"Auditing update: {item.Id}");
    public void Delete(Entity item) 
        => Console.WriteLine($"Auditing deletion: {item.Id}");
}

IWriteOnlyRepository<Entity> entityAuditor = new EntityAuditor();
// βœ… Can use Entity auditor for specific User operations:
IWriteOnlyRepository<User> userAuditor = entityAuditor;
userAuditor.Add(new User { Id = 1, Username = "john" });

The PECS Principle πŸ₯€

Java developers use a helpful mnemonic that translates well to C#:

PECS: Producer Extends, Consumer Super

In C# terms:

  • Producer β†’ out (covariant): When you're producing/returning values of type T
  • Consumer β†’ in (contravariant): When you're consuming/accepting values of type T

🧠 Visual Memory Aid

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚  PRODUCER (out) β†’ Covariance        β”‚
β”‚  β”Œβ”€β”€β”€β”€β”€β”€β”€β”                          β”‚
β”‚  β”‚ Box   β”‚ ──→ T  (gives you T)     β”‚
β”‚  β””β”€β”€β”€β”€β”€β”€β”€β”˜                          β”‚
β”‚  More specific box β†’ More general   β”‚
β”‚  Box β†’ Box βœ…          β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ CONSUMER (in) β†’ Contravariance β”‚ β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β” β”‚ β”‚ β”‚ Box β”‚ ←── T (takes T from you)β”‚ β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ β”‚ More general box β†’ More specific β”‚ β”‚ Box β†’ Box βœ… β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

Common Mistakes ⚠️

Mistake 1: Trying to Make Mutable Collections Covariant

// ❌ WRONG: This won't compile
IList<Cat> cats = new List<Cat>();
IList<Animal> animals = cats;  // Compiler error!

// Why? IList<T> is invariant because it has both:
// T this[int index] { get; set; }  // Output
// void Add(T item);                // Input

Fix: Use IEnumerable<T> when you only need to read:

IEnumerable<Cat> cats = new List<Cat>();
IEnumerable<Animal> animals = cats;  // βœ… Works!

Mistake 2: Confusing Variance Direction

// ❌ WRONG: Backwards thinking
public interface IProducer<in T>  // Wrong! Should be 'out'
{
    T GetValue();  // Compiler error: covariant type in output position
}

πŸ’‘ Tip: Let the compiler guide you! If you use in when you should use out, you'll get a clear error message.

Mistake 3: Forgetting That Arrays Are Covariant (and Broken!)

// ⚠️ DANGER: Arrays are covariant in C# (for historical reasons)
Cat[] cats = new Cat[10];
Animal[] animals = cats;  // Compiles, but dangerous!

animals[0] = new Dog();  // πŸ’₯ Runtime exception: ArrayTypeMismatchException

Fix: Use generic collections instead:

List<Cat> cats = new List<Cat>();
// IList<Animal> animals = cats;  // βœ… Compiler catches this!
IEnumerable<Animal> animals = cats;  // βœ… Safe covariant conversion

Mistake 4: Using Variance with Value Types

IEnumerable<int> numbers = new List<int> { 1, 2, 3 };
// IEnumerable<object> objects = numbers;  // ❌ Doesn't work!

// Why? Value types (int) don't have an inheritance relationship
// with reference types (object) for variance purposes.

Note: Variance only applies to reference type conversions. Boxing/unboxing is different from variance.

Mistake 5: Overusing Variance Annotations

// ❌ Premature variance:
public interface IMyInterface<out T>  // Added 'out' "just in case"
{
    // Empty interface or methods added later might need T as input!
}

πŸ’‘ Best Practice: Only add variance annotations when you have a clear use case. You can always add them later (it's a non-breaking change to make a type more variant).

Variance Rules Summary πŸ“‹

Position Covariant (out) Contravariant (in) Invariant (none)
Return types βœ… Allowed ❌ Forbidden βœ… Allowed
Method parameters ❌ Forbidden βœ… Allowed βœ… Allowed
Property getters βœ… Allowed ❌ Forbidden βœ… Allowed
Property setters ❌ Forbidden βœ… Allowed βœ… Allowed
Out parameters βœ… Allowed ❌ Forbidden βœ… Allowed
In parameters ❌ Forbidden βœ… Allowed βœ… Allowed

When to Use Variance 🎯

Use Covariance (out) when:

  • Creating read-only interfaces (repositories, queries)
  • Designing factory patterns
  • Working with immutable data structures
  • Building LINQ-like query operators
  • Defining event result types

Use Contravariance (in) when:

  • Creating comparison/validation logic
  • Designing visitor patterns
  • Building handler/consumer interfaces
  • Writing command processors
  • Defining event handler delegates

Use Invariance (no annotation) when:

  • The type is used in both input and output positions
  • You need mutable collections or properties
  • You're unsure (you can add variance later)

Key Takeaways πŸŽ“

  1. Covariance (out) allows using a more derived type; safe for outputs only (producers)
  2. Contravariance (in) allows using a more general type; safe for inputs only (consumers)
  3. Invariance (no annotation) is required when a type appears in both input and output positions
  4. Variance only works with reference types and interfaces/delegates, not classes or structs
  5. Arrays are covariant but unsafeβ€”use generic collections instead
  6. The compiler enforces variance rulesβ€”if you misuse in or out, you'll get a compile error
  7. PECS principle: Producer = out, Consumer = in
  8. IEnumerable<T>, IEnumerator<T>, and Func<T> are covariant
  9. Action<T>, IComparer<T>, and IComparable<T> are contravariant
  10. IList<T>, List<T>, and Dictionary<TKey,TValue> are invariant

πŸ“‹ Quick Reference Card

Concept Syntax Example Use Case
Covariance interface I<out T> IEnumerable<Cat> β†’ IEnumerable<Animal> Read-only collections, producers
Contravariance interface I<in T> IComparer<Animal> β†’ IComparer<Cat> Comparers, handlers, consumers
Invariance interface I<T> IList<Cat> β‰  IList<Animal> Mutable collections, read-write

Memory Trick:

  • πŸ”Ί OUTput β†’ COvariant (more specific β†’ more general)
  • πŸ”» INput β†’ CONTRAvariant (more general β†’ more specific)

πŸ€” Did You Know?

The concept of variance in type systems dates back to the 1960s, but it wasn't widely adopted in mainstream programming languages until the 2000s. C# 4.0 (released in 2010) was the first version to support variance for interfaces and delegates. The feature was carefully designed to maintain backward compatibility while enabling more flexible and expressive code. Java got similar features around the same time with wildcards (? extends and ? super), though the syntax differs from C#'s approach!

πŸ“š Further Study

  1. Microsoft Docs - Covariance and Contravariance (C#): https://docs.microsoft.com/en-us/dotnet/csharp/programming-guide/concepts/covariance-contravariance/
  2. Eric Lippert's Blog Series on Variance: https://ericlippert.com/2007/10/16/covariance-and-contravariance-in-c-part-one/
  3. Variance in Generic Interfaces (Deep Dive): https://docs.microsoft.com/en-us/dotnet/standard/generics/covariance-and-contravariance