C# Covariance and Contravariance: in and out Explained
A List<Dog> is not a List<Animal> — but an IEnumerable<Dog> is an IEnumerable<Animal>. The difference is one keyword: out.
You know that a Dog is an Animal — that's ordinary inheritance, and it means a Dog reference can always go wherever an Animal reference is expected. So it feels completely natural to expect this to compile:
List<Dog> dogs = new List<Dog>();
List<Animal> animals = dogs; // compile error — does not compile!And yet, this one compiles just fine:
List<Dog> dogs = new List<Dog>();
IEnumerable<Animal> animals = dogs; // compiles!Same dogs variable, same underlying list, same "a Dog is an Animal" relationship — one assignment is rejected, the other is accepted. That's not an inconsistency in C#; it's a deliberate, carefully-designed rule called variance, and once you see why the second one is actually safe while the first genuinely isn't, the whole thing clicks into place.
In this lesson, you'll understand exactly why List<Dog> can't be a List<Animal>, how out and in let a generic interface's type parameter flex safely along an inheritance hierarchy, and how to recognize covariance and contravariance in the interfaces you already use every day. This is a solid first look — the deeper compiler internals of variance are covered later, in the Advanced module.
What Is It?
The Simple Explanation
Variance is the rule that decides whether SomeGeneric<Dog> can be used wherever SomeGeneric<Animal> is expected (or the other way around), given that Dog is an Animal. For most generic types, the answer is simply "no" — List<Dog> and List<Animal> are treated as two completely unrelated types, despite Dog and Animal being related. But for certain generic interfaces, C# lets you opt in to a controlled, safe form of flexibility: covariance (marked with out) lets a more derived type parameter substitute for a less derived one, and contravariance (marked with in) lets it work the other way around.
The Technical Definition
A generic interface's type parameter can be declared with a variance annotation:
out T(covariant) — the type parameter may only appear in output positions (return types). In exchange,IInterface<Derived>is treated as assignable toIInterface<Base>, preserving the direction of the inheritance relationship.in T(contravariant) — the type parameter may only appear in input positions (method parameters). In exchange,IInterface<Base>is treated as assignable toIInterface<Derived>, reversing the direction of the inheritance relationship.- No annotation (invariant) — the default.
SomeGeneric<Derived>andSomeGeneric<Base>are entirely unrelated types, regardless of the relationship betweenDerivedandBase. Almost every generic class you've used so far —List<T>,Dictionary<TKey,TValue>— is invariant.
Variance annotations can only be declared on generic interfaces and delegates — never on classes or structs. That restriction isn't arbitrary; the "Under the Hood" section later in this lesson explains exactly why.
Why Does It Exist?
The Problem
Consider why List<Dog> dogs; List<Animal> animals = dogs; genuinely cannot be allowed to compile — not because C# is being unnecessarily strict, but because it would let you break type safety at runtime. If it were allowed:
List<Dog> dogs = new List<Dog>();
List<Animal> animals = dogs; // if this were legal...
animals.Add(new Cat()); // ...this would compile — Cat is an Animal!
Dog firstDog = dogs[0]; // InvalidCastException — there's a Cat in thereBecause animals and dogs would be the exact same list object, adding a Cat through the animals reference would silently corrupt the dogs list with something that isn't a Dog at all. The compiler has no way to catch this at compile time once it's allowed the assignment — so it refuses the assignment itself, up front, rather than let a runtime type error sneak in later. This is exactly why List<T> is invariant.
But now consider a read-only view — something that can only ever hand you items, never accept new ones:
IEnumerable<Dog> dogs = new List<Dog> { new Dog(), new Dog() };
IEnumerable<Animal> animals = dogs; // this is genuinely safe
foreach (Animal a in animals) { /* every item really is an Animal (a Dog, specifically) */ }There's no Add method on IEnumerable<T> to exploit — you can only read items out, and every Dog you read out genuinely is an Animal. Nothing unsafe can happen here.
The Solution
Variance annotations let the compiler tell these two situations apart, purely from the interface's shape. If a type parameter is only ever handed out (as a return value) and never accepted in (as a parameter), the compiler can prove that widening from Derived to Base is safe, and marks it out T — this is covariance. If a type parameter is only ever accepted in and never handed back out, narrowing the other direction is provably safe too, marked in T — this is contravariance. List<T> can't be marked either way, because it both accepts items (Add) and hands them out (the indexer, enumeration) — mixing both directions is exactly what made the unsafe scenario above possible.
Big Picture
out TDog is an Animal↓ same direction ↓
IEnumerable<Dog> is an IEnumerable<Animal>
in TDog is an Animal↓ reversed ↓
IComparer<Animal> is an IComparer<Dog>
List<T>) allow neither direction — they both accept and hand out T.
How It Works
public interface IEnumerable<out T> // covariant — note the "out"
{
IEnumerator<T> GetEnumerator(); // T only appears as a return type
}
public interface IComparer<in T> // contravariant — note the "in"
{
int Compare(T x, T y); // T only appears as parameters
}
IEnumerable<T>, which you met in the previous module, is covariant —Tonly ever flows out, throughGetEnumerator()and, transitively,Current.
- For
out Tto be legal, every member of the interface must useTonly in output positions — return types, oroutparameters. Not one single input use is allowed anywhere in the interface. - For
in Tto be legal, every member must useTonly in input positions — ordinary parameters. Not one single output use anywhere.
- You can't just slap
outonto any interface's type parameter and hope for the best — if any member would violate the direction, the interface itself fails to compile. This guarantee is what makes the resulting assignability rule provably safe, not just "usually fine."
- Once an interface is validly marked
out Torin T, the compiler automatically allows the corresponding widened or narrowed assignment everywhere that interface is used — you don't do anything further to "activate" it.
Simple Example
public class Animal { public string Name { get; init; } = ""; }
public class Dog : Animal { }
public class Cat : Animal { }
// ─── Covariance: out T ───
List<Dog> dogList = [new Dog { Name = "Rex" }, new Dog { Name = "Fido" }];
IEnumerable<Dog> dogsEnumerable = dogList;
IEnumerable<Animal> animalsEnumerable = dogsEnumerable; // covariance — widening is safe
foreach (Animal a in animalsEnumerable)
Console.WriteLine(a.Name); // Rex, Fido — reading only, nothing can go wrong
// List<Dog> itself is still invariant — this still doesn't compile:
// List<Animal> notAllowed = dogList; // compile error
// ─── Contravariance: in T ───
public class AnimalNameComparer : IComparer<Animal>
{
public int Compare(Animal? x, Animal? y) =>
string.Compare(x?.Name, y?.Name, StringComparison.Ordinal);
}
IComparer<Animal> animalComparer = new AnimalNameComparer();
IComparer<Dog> dogComparer = animalComparer; // contravariance — narrowing is safe
dogList.Sort(dogComparer); // an Animal-comparer works perfectly well for comparing DogsCode → Meaning → Result:
IEnumerable<Dog>widens toIEnumerable<Animal>because the interface only ever handsTout — the inheritance direction is preserved.List<Dog>itself still cannot widen toList<Animal>—List<T>is a class, and classes can't declare variance at all, plus it both reads and writesT.IComparer<Animal>narrows toIComparer<Dog>because the interface only ever acceptsTin — anAnimal-comparer is perfectly capable of comparing twoDogs, since everyDoggenuinely is anAnimal.
Real-World Example
A Repository<T>-style method that returns products, and a generic comparer used to sort a product catalog, are realistic places variance shows up without you necessarily noticing.
public class Product { public string Name { get; init; } = ""; public decimal Price { get; init; } }
public class DiscountedProduct : Product { public decimal DiscountPercent { get; init; } }
public class ProductRepository
{
private readonly List<DiscountedProduct> _discounted = [];
public void Add(DiscountedProduct product) => _discounted.Add(product);
// Returns IEnumerable<DiscountedProduct>, but callers only asking for
// IEnumerable<Product> can accept it directly, thanks to covariance:
public IEnumerable<DiscountedProduct> GetAll() => _discounted;
}
void PrintCatalog(IEnumerable<Product> products) // accepts ANY covariant-compatible source
{
foreach (Product p in products)
Console.WriteLine($"{p.Name}: {p.Price:C}");
}
var repo = new ProductRepository();
repo.Add(new DiscountedProduct { Name = "Headphones", Price = 59.99m, DiscountPercent = 10 });
PrintCatalog(repo.GetAll()); // IEnumerable<DiscountedProduct> flows into IEnumerable<Product>
// A general-purpose price comparer for ANY Product works for the more specific
// DiscountedProduct too, thanks to contravariance:
public class PriceComparer : IComparer<Product>
{
public int Compare(Product? x, Product? y) => (x?.Price ?? 0).CompareTo(y?.Price ?? 0);
}
List<DiscountedProduct> discounted = [.. repo.GetAll()];
discounted.Sort(new PriceComparer()); // IComparer<Product> accepted where IComparer<DiscountedProduct> is neededNeither of these would compile without variance — GetAll() deliberately returns the more specific IEnumerable<DiscountedProduct> (good design: be specific about what you return), yet callers who only care about the general Product shape can consume it directly. And a single, general-purpose comparer can be reused for any more specific product type, without writing a new comparer for every subclass.
Analogy
Think of a covariant IEnumerable<Dog> as a vending machine that only ever dispenses items — you can't put anything into it, only take things out. If a vending machine dispenses specifically dog treats, you can safely treat it as "a machine that dispenses animal treats" — everything that comes out really is an animal treat, just a more specific kind. Nothing bad happens by looking at it more generally.
Now think of a contravariant IComparer<Animal> as a mailbox that only ever accepts mail — you can't take anything out, only put things in. A mailbox built to accept any piece of "animal mail" can obviously also accept the more specific "dog mail" — accepting the general case means you can already handle every specific case. That's why the relationship reverses: a handler for the broader category works for the narrower one, not the other way around.
A regular List<T>, by contrast, is like a two-way delivery slot — you can both drop things in and take things out. Mixing those two directions is exactly what makes it unsafe to treat as anything other than exactly the type it was declared with.
Under the Hood
List<T>is a concrete class with real storage — an internal array holding actualTvalues. ItsAddmethod genuinely needs to accept aTas input, and its indexer genuinely needs to hand one back out, so it structurally can never satisfy the "only in" or "only out" requirement variance demands. Interfaces and delegates, by contrast, only describe a contract — no storage of their own — which is what makes it possible for the compiler to check every member's direction cleanly.
- Assigning a covariant or contravariant reference doesn't create a new object or copy anything — it's the exact same underlying object, just viewed through a differently-typed reference the compiler has proven is safe to use that way.
IEnumerable<out T>,IEnumerator<out T>, andIReadOnlyCollection<out T>are all covariant — notice they're all read-only shaped, with no way to add items.IComparer<in T>,IEqualityComparer<in T>, andAction<in T>are all contravariant — they only ever consumeT, never hand it back.
- How variance interacts with value types (it doesn't apply directly — boxing is involved), variance on delegate type parameters in depth, and the formal proof of why the "only in" / "only out" rule is sound are all covered later in the Advanced module, once you've had more practice recognizing variance in everyday interfaces first.
Common Confusion
1. "If Dog is an Animal, shouldn't List<Dog> always be a List<Animal>?" — no, and now you know why
This is the single most common assumption learners bring to variance, and it's wrong specifically because List<T> both reads and writes T. The "Why Does It Exist?" section above walks through exactly the bug that assumption would introduce — an Animal-typed reference letting you smuggle a Cat into a list of Dogs.
2. out here is not the same out as an out parameter
You've seen out before as a parameter modifier (void TryParse(string s, out int result)). This is a different, unrelated use of the same keyword — on a generic type parameter, out means "covariant," describing how the type parameter is allowed to be used across the whole interface, not a single parameter's passing convention.
3. Which direction is "co" and which is "contra"? A quick anchor
Covariant moves in the same direction as the inheritance relationship (Dog → Animal, so IEnumerable<Dog> → IEnumerable<Animal>). Contravariant moves in the opposite direction (IComparer<Animal> → IComparer<Dog>, reversed from Dog → Animal). If you remember "out flows out and forward, in flows backward," the rest follows.
Common Mistakes
Mistake 1 — Expecting a concrete generic class to be variant
Wrong — doesn't compile, because List<T> is invariant:
List<Animal> animals = new List<Dog>(); // compile errorCorrect — use a covariant interface reference instead, if you only need to read:
IEnumerable<Animal> animals = new List<Dog>(); // compilesMistake 2 — Trying to add an out T type parameter as a method parameter
This won't compile — it violates the "only in output positions" rule that makes out T legal in the first place:
public interface IProducer<out T>
{
T Produce();
void Consume(T item); // compile error — T used as input on a covariant parameter
} Split the responsibilities, or drop the variance annotation if the interface genuinely needs to do both — this is exactly the tension that keeps List<T> invariant.
Mistake 3 — Assuming variance is a runtime conversion that copies data
Thinking IEnumerable<Animal> animals = dogs; somehow creates a new collection or copies elements into an "animal-shaped" container. It's the same object the whole time — variance is purely a compile-time typing rule about which reference types are considered compatible, not a data transformation.
When Should I Use It?
Recognize and rely on variance when
- You're writing a method that only needs to read a sequence of items — accept
IEnumerable<T>rather than a concreteList<T>, so callers with more specific types can pass them in directly. - You have a general-purpose comparer or equality comparer and want it to work for a more specific subtype too — no extra code needed, contravariance already covers it.
Don't reach for it when
- You're designing your own generic interface with genuinely mixed read/write members — most real designs, like
List<T>itself, are correctly invariant, and forcing variance where it doesn't structurally fit won't compile anyway. - You need the deeper mechanics (variance on your own custom interfaces, delegate variance in depth) — that's covered later in the Advanced module; for now, recognizing it in
IEnumerable<T>,IComparer<T>, and similar built-in interfaces is the goal.
T, expect it to be covariant (out T). If it only ever accepts T as a parameter, expect it to be contravariant (in T). If it does both, expect it to be invariant — and that's fine; invariance is the safe, correct default for anything that both reads and writes.
Mental Model
out T) = only hands T out → same direction as inheritance → IEnumerable<Dog> is an IEnumerable<Animal>Contravariant (
in T) = only takes T in → reversed direction → IComparer<Animal> is an IComparer<Dog>Invariant (no annotation) = does both → no relationship at all →
List<Dog> is never a List<Animal>Remember:
· Variance is a compile-time typing rule, not a runtime conversion — the object never changes.
· Only interfaces and delegates can declare variance — classes like
List<T> never can.· "Out flows out and forward, in flows backward" — a quick anchor for which direction each keyword allows.
Key Takeaway
List<Dog>is never aList<Animal>— generic classes are invariant, because allowing that assignment would let you insert the wrong type through the wider reference and corrupt the original list.- Covariance (
out T) is safe on interfaces that only ever handTout, likeIEnumerable<T>—IEnumerable<Dog>is genuinely anIEnumerable<Animal>. - Contravariance (
in T) is safe on interfaces that only ever acceptTin, likeIComparer<T>—IComparer<Animal>works perfectly as anIComparer<Dog>. - Only generic interfaces and delegates can declare variance — never classes or structs, since concrete storage inherently needs both directions.
- This is a first look — the compiler internals and variance on custom interfaces get their full depth in the Advanced module.
Check Your Understanding
You've seen why List<Dog> isn't a List<Animal>, but IEnumerable<Dog> is an IEnumerable<Animal>. Let's check your understanding.
1. Why does List<Animal> animals = new List<Dog>(); fail to compile?
Show answer
Correct: B
Why B is correct: As shown in "Why Does It Exist?", if this assignment were legal, you could call animals.Add(new Cat()) and silently corrupt the underlying List<Dog> with a Cat — a runtime type violation the compiler refuses to allow.
Why A is incorrect: The example assumes Dog : Animal, which is exactly why this looks like it should work — the problem is about invariance, not the inheritance relationship itself.
Why C is incorrect: Some generic interfaces, like IEnumerable<T>, do support exactly this kind of flexibility via covariance — it's specific to List<T> being invariant, not a blanket rule.
Why D is incorrect: This describes nothing about how generics actually work — a program can have many differently-typed List<T> instances simultaneously.
Reinforcement: Invariance exists specifically to prevent inserting the wrong type through a wider reference to the same underlying collection.
2. What does the out keyword mean when applied to a generic interface's type parameter, as in IEnumerable<out T>?
Show answer
Correct: B
Why B is correct: out T is the covariance annotation — it restricts T to output positions across every member of the interface, and in exchange the compiler allows widening assignments that follow the same direction as the inheritance relationship.
Why A is incorrect: This confuses two unrelated uses of the keyword out — the parameter modifier (out int result) is different from the generic variance annotation, as covered in "Common Confusion."
Why C is incorrect: It's fully enforced at compile time — both restricting how T can be used inside the interface, and enabling the resulting assignability rule.
Why D is incorrect: Variance has nothing to do with whether implementers are classes or structs.
Reinforcement: out T on a type parameter is a distinct concept from out as a parameter modifier — same keyword, different meaning depending on where it's used.
3. Given public class Vehicle {} and public class Car : Vehicle {}, which of the following compiles, assuming IComparer<in T>'s contravariance?
Show answer
Correct: A
Why A is correct: Contravariance reverses the direction — a comparer for the more general Vehicle can stand in for a comparer of the more specific Car, because it's already capable of comparing anything that's a Vehicle, Car included. This matches the IComparer<Animal> → IComparer<Dog> example from this lesson.
Why B is incorrect: This reverses the actual rule — a Car-only comparer can't safely be used to compare arbitrary Vehicles, since it may only know how to compare specifically Car details.
Why C is incorrect: IComparer<T> is explicitly declared in T in the BCL, precisely so this kind of narrowing assignment is allowed.
Why D is incorrect: Only one direction is sound for a contravariant interface — allowing both would reopen the same kind of type-safety hole invariance exists to prevent.
Reinforcement: Contravariance always flows opposite to the inheritance relationship: the more general handler substitutes for the more specific one, never the reverse.
4. Why can a class like List<T> never be declared covariant or contravariant, no matter how it's written?
Show answer
Correct: B
Why B is correct: C# restricts variance annotations to interfaces and delegates. Beyond the syntax restriction, it wouldn't help anyway — as explained in "Under the Hood," a genuinely mutable, storage-backed type like List<T> structurally needs to both accept T (Add) and hand it out (the indexer), which directly conflicts with the "only in" or "only out" rule variance requires.
Why A is incorrect: The restriction isn't an incidental IL detail — it reflects a genuine structural reason classes with real read/write storage can't safely support variance.
Why C is incorrect: This is intentional, documented language design, not a defect.
Why D is incorrect: Variance is unrelated to value type vs. reference type — it's about whether a type parameter flows only in, only out, or both.
Reinforcement: Variance is reserved for interfaces and delegates because only a pure contract (no backing storage) can cleanly guarantee a type parameter flows in just one direction.
You now understand why some generic types flex safely along inheritance hierarchies and others don't. Next: connecting back to the collections you already know — List, Dictionary, HashSet, Stack, and Queue — now explained as the generic types they truly are, plus new ones worth adding to your toolkit.