Variance is the set of rules that decide when one generic type is assignable to another. Specifically, when Product inherits from Item, is IEnumerable<Product> also an IEnumerable<Item>? The short answer is "sometimes, and only if you ask for it explicitly with out or in." This chapter walks through why the default answer is "no," when the compiler will say "yes," and how the out and in keywords give you control over the trade-off.
Consider a small e-commerce app with a base class for anything sellable, and a more specific class for actual products. The full hierarchy used throughout this chapter:
Product inherits from Item. DigitalProduct and PhysicalProduct both inherit from Product. Standard single inheritance.
Now consider a method that sends notifications about a batch of items:
The method only reads from the sequence. It never adds to it, never replaces items, never writes anything back. So intuitively, calling Notify with a List<Product> should work fine, since every Product is also an Item.
This compiles and runs. The reason is variance, and the rest of this chapter explains exactly how the compiler decides to accept that call.
But before we get to the "yes" case, consider a slightly different version that doesn't compile:
Same hierarchy, same intuition (every Product is an Item), but the compiler refuses. The difference between the two cases is the whole point of this chapter.
The default behavior of generics is invariance: List<Product> is not a List<Item>, and List<Item> is not a List<Product>. The two types are unrelated as far as the type system is concerned, even though their type arguments are related.
Invariance exists to keep assignment type-safe. To see why, imagine the compiler did let you assign List<Product> to List<Item>. Look what would happen next:
The variable items thinks it holds a List<Item>, so adding a plain Item looks fine. But the underlying list is actually a List<Product>, and now there's a non-Product sitting inside it. The next read of products[0] as a Product either crashes or corrupts state without warning.
The compiler prevents this by refusing the assignment in the first place. List<T> permits both reads and writes of T, so making it assignable in either direction breaks the type system. Invariance is the safe default because most generic types do both reads and writes.
Variance is a compile-time annotation. It's enforced by the compiler when the type is declared and the assignment is made. There is no runtime overhead, no extra checks, no slower dispatch. The JIT-compiled code for a covariant interface call is identical to the code for an invariant one.
Variance is only an option when the type's role is strictly one-directional. A type that only hands out values of T can safely widen T. A type that only accepts values of T can safely narrow T. The keywords out and in indicate which direction the type uses.
out KeywordCovariance is the rule that says "if Derived is assignable to Base, then Generic<Derived> is assignable to Generic<Base>." Same direction as the type arguments. The classic example is IEnumerable<T>:
That out T in the declaration is the whole story. It tells the compiler: "T is only ever produced from this interface, never consumed." Read it as "out of the interface." The interface gives values out, it doesn't take them in.
Because nothing flows in, widening T is safe. If you have an IEnumerable<DigitalProduct> and you assign it to an IEnumerable<Product>, the only thing a caller can do is iterate and get values. Every value will be a DigitalProduct, which is also a Product, so the caller sees what it expects.
Here's the assignment that wasn't legal with List<T>, working perfectly with IEnumerable<T>:
The chain of reasoning is: List<DigitalProduct> implements IEnumerable<DigitalProduct>. IEnumerable<T> is covariant in T. DigitalProduct is assignable to Item. Therefore IEnumerable<DigitalProduct> is assignable to IEnumerable<Item>, which is what Notify asks for.
A picture helps. Covariance preserves the direction of the original inheritance:
The arrows on the left show the class hierarchy. The arrows on the right show the assignability between the generic types. Both go the same direction. That's covariance, and the out T annotation is what unlocks it.
Many built-in interfaces are covariant for the same reason:
Anything whose name has "Read-Only" or "Enumerable" or "Func" in it is a strong candidate for covariance, because reading and returning are the only things they do.
outThe out keyword isn't a wish, it's a contract the compiler enforces at the interface declaration. If you mark T as out, every member of the interface has to use T only in output positions. Trying to put T in an input position is a compile error.
What's wrong with this code?
The parameter item is an input position. If the interface said "T only flows out," and then Add took a T as input, the contract would be a lie. The compiler refuses to let you declare it.
To see why this matters, imagine the compiler didn't catch it:
The same hole as the List<T> example, just dressed up in interface clothing. The out keyword forces interface designers to keep T strictly outbound, which is what makes the assignment safe.
Where can T appear in an out-annotated interface? Only in return types and out parameters of methods. T can be the return type of a method, the type of a read-only property, or the return type of a delegate. Anywhere a value of T is flowing from the interface to the caller.
Where can T not appear? Method parameters, in parameters, ref parameters, settable properties, event types, and as a generic argument to anything that isn't itself covariant. All input or in-and-out positions.
A property is a useful edge case. A getter is fine. A setter is not.
The compiler is happy with this. The setter is commented out because adding it would put T in an input position and break covariance.
in KeywordContravariance is the rule that says "if Derived is assignable to Base, then Generic<Base> is assignable to Generic<Derived>." The direction flips. The most common example is IComparer<T>:
The in T reads as "into the interface." The interface only accepts values of T, it never produces them. The return type of Compare is int, not T. Both parameters are inputs.
When a type only consumes T, narrowing T is safe. If you have an IComparer<Item>, then it knows how to compare any two Item values. Since every Product is an Item, that same comparer can compare any two Product values too. So IComparer<Item> is assignable to IComparer<Product>, even though Item is the more general type.
Let's see it in action with a price comparer:
byPrice was declared as IComparer<Item>. The Sort call needs an IComparer<Product>. The assignment works because IComparer<T> is contravariant in T. Anything that can compare two Items can compare two Products, because every Product is already an Item.
Now the second diagram, showing how contravariance flips the direction:
The class hierarchy on the left still flows from specific to general. The assignability on the right flows in the opposite direction. That's contravariance: the type parameter direction is reversed compared to the underlying inheritance.
Other contravariant interfaces in the BCL include IEqualityComparer<in T> and the delegate Action<in T>:
Both only accept T. Neither returns it. So both flip the inheritance direction.
inin is the mirror image of out. If you mark T as in, every member of the interface has to use T only in input positions. Trying to return T, or use it as the type of a read-only property, is a compile error.
What's wrong with this code?
Create() returns a T. Return types are output positions. If T is supposed to only flow in, returning one breaks the contract.
To see why the rule is necessary, picture what would happen if it didn't apply:
The variable productFactory thinks it creates Products. The underlying factory only creates plain Items. The cast back to Product fails. The compiler heads this off by refusing to mark a return position with in T.
Where can T appear in an in-annotated interface? Method parameters (regular or in), and as a generic argument to a contravariant position of another type. Anywhere T flows from the caller into the interface.
Where can T not appear? Return types, read-only property types, out parameters, event types, and most generic arguments. Anything that would let T leak out.
Things get subtle when one variant type is used inside another. The compiler still tracks the direction of T, but it inverts the rules every time T passes through a contravariant position. The mental model is the same one you use for negation in logic: a negative inside a negative is a positive.
Consider this declaration:
TResult is covariant. It appears in the return type of Run (output position) and as the type argument of IEnumerable<TResult> in the return type of RunAll. Since IEnumerable<out T> is itself covariant, plugging a covariant parameter into a covariant slot keeps the direction. The compiler accepts it.
TQuery is contravariant. It appears as a parameter type on Run (input position) and as the type argument of IEnumerable<TQuery> in the parameter of RunAll. Here's where the inversion matters. The parameter slot of RunAll is an input position, which is fine for an in TQuery. But the type argument of IEnumerable<TQuery> is a covariant slot, and the surrounding parameter is itself an input position. The compiler walks the nested positions: contravariant parameter, covariant out T slot inside it. Two flips? No, just one. The result is that TQuery appears in a contravariant position overall, which is exactly what in TQuery requires. The interface compiles.
The rule of thumb: every time you pass through a contravariant slot, the direction flips. Pass through it twice and you're back where you started. Pass through it once and the direction reverses. For everyday code, the compiler does this for you and just tells you whether the declaration is legal. But it helps to know the rule when you're designing a complex generic API and the compiler refuses something that looks right.
What's wrong with this code?
This one looks innocent. T appears as the input type of Func<T, string>, which is a contravariant slot. So if T is in a contravariant slot, and Func is being returned (an output position), the overall position of T is "contravariant inside covariant," which flips to covariant. That conflicts with the in T declaration. The compiler rejects it with CS1961: Invalid variance: The type parameter 'T' must be contravariantly valid on 'IPipeline<T>.CreateRenderer()'. 'T' is covariant.
To fix it, either drop the in (making the interface invariant), or change the method so T doesn't end up in an effectively covariant position:
Now T is straightforwardly contravariant, and the declaration is legal.
This variance analysis is entirely a compile-time concern. The CLR doesn't track variance at runtime, and the JIT-compiled code for a variant interface call is no different from any other interface call. The only "cost" of nested variance is the extra work the compiler does to verify the declaration.
Classes can't be variant. class List<out T> and class Bag<in T> don't compile. The compiler rejects them. Variance is restricted to interfaces and delegate types.
The reason is that classes have implementation, not just contracts. A class can have fields, private state, virtual methods, and overrides, all of which can use T in any direction. Trying to retrofit variance onto something that mixes inputs and outputs would either require huge restrictions on what classes can do, or open exactly the safety holes invariance prevents. The language designers picked the simpler rule: interfaces and delegates only.
Here are the variance-eligible declarations you'll meet most often:
| Construct | Variance allowed? | Example |
|---|---|---|
| Interface type parameter | Yes (in, out, or neither) | IEnumerable<out T> |
| Delegate type parameter | Yes (in, out, or neither) | Func<in T, out TResult> |
| Class type parameter | No | List<T> is always invariant |
| Struct type parameter | No | KeyValuePair<TKey, TValue> |
| Method generic parameter | No | T Identity<T>(T x) |
A struct can't be variant for the same reason a class can't. A method's generic parameter is a separate concept (type inference), not variance, and the question doesn't apply.
Even when an interface is declared out or in, the variance only applies when the type argument is a reference type. Value types stay invariant no matter what.
What's wrong with this code?
IEnumerable<T> is covariant. int is assignable to object through boxing. So shouldn't IEnumerable<int> be assignable to IEnumerable<object>?
It isn't, because variance is about reference assignment, and reference assignment for value types doesn't exist without boxing. Boxing creates a new object on the heap and copies the value into it. The variance guarantee, "you can treat a reference to Derived as a reference to Base without any conversion," doesn't hold for value types: there's no shared reference to begin with, and turning an int into an object requires real work at runtime.
If the compiler did let the assignment through, iterating the sequence would have to box every int on the fly. That's a behavior change, not a free conversion, and the variance design specifically excludes it.
The fix is explicit projection:
Cast<object> boxes each int as it's enumerated. The cost is one heap allocation per element, paid lazily during iteration. That's what the type system was preventing.
The same restriction applies to contravariance: an IComparer<object> is not an IComparer<int>, even though int derives from object in the type system. The variance only applies when both type arguments are reference types.
When variance does apply, the conversion is free. IEnumerable<DigitalProduct> to IEnumerable<Product> is a reinterpretation of the existing reference, with no allocation and no copying. The same conversion for value types via Cast<object> allocates a heap object per element, the difference between zero cost and per-element cost.
Long before generics existed, arrays in .NET were already covariant. Product[] is assignable to Item[], and the compiler has always allowed it.
So far so good. But the trouble starts the moment you try to write to the array through the wider reference:
The variable items is typed as Item[], so the compiler thinks the assignment is fine. The CLR knows the actual array is a Product[], refuses to let a plain Item in, and throws at runtime. Array covariance leaks the safety hole that variance on generic interfaces is designed to prevent.
The reason the language ships this anyway is partly historical (Java had array covariance, and .NET copied the model in 2002), partly practical (many APIs took object[], and array covariance let you pass any reference array to them without conversion). The price is a runtime type check on every array store. Every time you assign into a reference-type array, the CLR verifies the element's type against the array's actual element type.
Array stores include a covariance check at runtime. For most code it's negligible, but it shows up in tight loops over reference-type arrays. The check is skipped for value-type arrays (which aren't covariant), and for arrays whose element type is sealed. JIT optimizations can also elide it when the compiler can prove the target type matches.
Generic collections fixed both problems. List<T> is invariant, so the runtime check disappears (because no covariant assignment is possible). IReadOnlyList<out T> and IEnumerable<out T> provide the assignability where needed, but only in directions that can't leak unsafe writes. To widen a parameter, prefer IEnumerable<T> or IReadOnlyList<T> over array covariance.
The second version achieves the same calling flexibility without the runtime hazard.
Func and Action: Variance on DelegatesDelegates can be variant in exactly the same way interfaces can. The built-in Func and Action families use both in and out together to give you flexibility on both sides of a function signature.
Here are the declarations the BCL uses for the one-argument forms:
Func<TResult> is covariant in TResult because the delegate only returns it. Action<T> is contravariant in T because the delegate only takes it. Func<T, TResult> is contravariant in T (the input) and covariant in TResult (the output). The two annotations coexist because they cover different type parameters.
A practical example: a function that returns the price of an item.
That attempted assignment is in the wrong direction. Func<Product, decimal> only knows how to handle Product, but Func<Item, decimal> callers might pass any Item. So the assignment goes the other way:
itemPrice was built to take any Item. The CalculateTax method asks for a Func<Product, decimal>. Since Product is a kind of Item, anything itemPrice can do, Func<Product, decimal> can also do. The contravariant in T parameter on Func<T, TResult> lets the assignment go through.
And it works the same way for the return type going the other direction:
The return value is always a DigitalProduct, which is also a Product, so callers expecting a Product see exactly what they expect.
When you write your own delegates, you can use in and out the same way:
The rules the compiler enforces are identical to interfaces: in parameters can only appear in input positions, out parameters can only appear in output positions, mixed-direction usage requires no variance keyword (and the type is invariant).
Here's a longer example that uses all three flavors of variance together. The scenario: an e-commerce search pipeline that filters products, ranks them, and notifies subscribers about the results.
Three variance points show up in one program. FilterAffordable accepts List<Product> (where it asked for IEnumerable<Item>) thanks to covariance. RankProducts accepts an IComparer<Item> (where it asked for IComparer<Product>) thanks to contravariance. NotifyEach accepts an Action<object> (where it asked for Action<Item>) thanks to contravariance on the delegate.
None of those conversions involve any runtime work. They're pure compile-time checks that the compiler resolved by reading the out and in annotations on the interface and delegate declarations.
When you write a new generic interface or delegate, the variance annotation is a design decision, not an afterthought. The choice answers a question about your API: who is allowed to substitute one type for another?
Three guidelines cover most situations:
in or out later if you discover a real need; removing them is a breaking change.out T. Read-only collections, factories that return a single value, query results, and event source interfaces all fit this pattern. The benefit is that callers can hold the result as a more general type, which lets methods declared with broader parameters accept it.in T. Comparers, validators, event handlers, and writers all fit. The benefit is that a single implementation built for a base type can be used wherever a more specific type is expected, which often eliminates the need for multiple implementations.The split-interface pattern from Exercise 10 is the standard escape hatch when an API does both. You expose a covariant read interface and a contravariant write interface, and the class implements both. Callers that only need to read get a covariant view; callers that only need to write get a contravariant view. The same class serves both with no duplication.
One last thing worth saying: variance only buys you flexibility for callers that already think in terms of base classes. If every caller of your API works with the exact concrete type, variance doesn't help anyone, and the extra annotation is noise. The time to add out or in is when you can point to actual callers or scenarios that would benefit from the wider assignment rules. Most internal APIs are perfectly happy to stay invariant.
A quick way to audit an existing interface for variance opportunities: list every member, mark each T usage as input (I) or output (O), and look at the resulting pattern.
out T.in T.This audit also doubles as documentation. If you can't articulate which positions T shows up in, you don't know your interface well enough to publish it.
A short reference for everything covered:
| Keyword | Position | Direction | Examples |
|---|---|---|---|
| (none) | n/a | Invariant | List<T>, Dictionary<TKey,TValue>, IList<T> |
out T | Output only | Covariant | IEnumerable<out T>, Func<out TResult> |
in T | Input only | Contravariant | IComparer<in T>, Action<in T> |
And the rules in one place:
out T means T is in output positions only (return types, read-only properties, out parameters).in T means T is in input positions only (regular parameters, settable property setters in non-variant types).ArrayTypeMismatchException. Prefer IReadOnlyList<T> or IEnumerable<T> for new code.in and out freely (Func<in T, out TResult>).9 quizzes