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 π
- Covariance (
out) allows using a more derived type; safe for outputs only (producers) - Contravariance (
in) allows using a more general type; safe for inputs only (consumers) - Invariance (no annotation) is required when a type appears in both input and output positions
- Variance only works with reference types and interfaces/delegates, not classes or structs
- Arrays are covariant but unsafeβuse generic collections instead
- The compiler enforces variance rulesβif you misuse
inorout, you'll get a compile error - PECS principle: Producer =
out, Consumer =in IEnumerable<T>,IEnumerator<T>, andFunc<T>are covariantAction<T>,IComparer<T>, andIComparable<T>are contravariantIList<T>,List<T>, andDictionary<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
- Microsoft Docs - Covariance and Contravariance (C#): https://docs.microsoft.com/en-us/dotnet/csharp/programming-guide/concepts/covariance-contravariance/
- Eric Lippert's Blog Series on Variance: https://ericlippert.com/2007/10/16/covariance-and-contravariance-in-c-part-one/
- Variance in Generic Interfaces (Deep Dive): https://docs.microsoft.com/en-us/dotnet/standard/generics/covariance-and-contravariance