C# Covariance vs Contravariance: How Generic Variance Works
c# covariance vs contravariance: Understand how C# covariance and contravariance control type conversions in generics and delegates, and when the compiler permits them...
Two generic types can be assignment-compatible even when their type arguments differ. IEnumerable<string> assigns to IEnumerable<object> without a cast, while List<string> does not assign to List<object> at all. The difference is variance: covariance, contravariance, and invariance describe which direction a type parameter may flow in a conversion, and the c# covariance vs contravariance distinction comes down to whether a type is produced, consumed, or both.
What Variance Means for Generic Types
Variance is a property of a type parameter declared on an interface or delegate. It tells the compiler whether a type argument can be replaced by a more derived type, a less derived type, or neither.
Covariance lets a type parameter accept a more derived type argument. Contravariance lets it accept a less derived one. Invariance means the type argument must match exactly.
The compiler permits variance only when it can prove that the conversion cannot lead to an unsafe write. That proof is based on where the type parameter appears in the member signatures.
| Form | Keyword | Direction | Typical use |
|---|---|---|---|
| Covariance | out | More derived to less derived | IEnumerable<T>, Func<T> |
| Contravariance | in | Less derived to more derived | IComparer<T>, Action<T> |
| Invariance | none | Exact match only | List<T> |
Covariance: Reading a More Derived Type Through an out Parameter
A covariant type parameter is declared with the out keyword. It may appear only in output positions: return types, get-only properties, and method return values.
public interface IRepository<out T> { T FindById(int id); }
Because the interface never accepts a T as input, an IRepository<Cat> can safely be treated as IRepository<Animal>. Any value read from it is already a Cat, which is also an Animal.
The framework's most familiar covariant type is IEnumerable<T>:
IEnumerable<string> names = new List<string> { "Ada", "Grace" }; IEnumerable<object> values = names;
This works because IEnumerable<T> exposes T only through GetEnumerator, which returns an IEnumerator<T> with a get-only Current property. T never appears as a method parameter, so the compiler can prove that no caller can write a value of the wrong type into the sequence.
Contravariance: Accepting a Less Derived Type Through an in Parameter
A contravariant type parameter is declared with the in keyword. It may appear only in input positions: method parameters and write-only properties.
public interface IComparer<in T> { int Compare(T x, T y); }
An IComparer<object> can compare any two objects, so it can certainly compare two strings. That is why IComparer<object> is assignable to IComparer<string>:
IComparer<object> objectComparer = Comparer<object>.Default; IComparer<string> stringComparer = objectComparer;
The direction is the reverse of covariance. Covariance widens what you can read from a value; contravariance widens what you can pass into a method.
Variance in Delegates
Delegates follow the same rules. Func<out TResult> is covariant in its result, and Action<in T> is contravariant in its argument.
Func<string> createString = () => "hello"; Func<object> createObject = createString; Action<object> printObject = obj => Console.WriteLine(obj); Action<string> printString = printObject;
Method group conversions also honor variance. A method that returns a derived type can be assigned to a delegate whose return type is a base type, and a method that accepts a base type can be assigned to a delegate that expects a derived type. This is why a single event handler with a broad parameter type can serve multiple narrower delegate signatures.
Arrays: The Legacy Covariance Exception
Arrays are covariant, but the safety is enforced at runtime rather than at compile time.
string[] strings = new string[10]; nobject[] objects = strings; objects[0] = 42;
The assignment compiles, and the write throws ArrayTypeMismatchException at runtime. The runtime inserts a type check on every element write. Generic variance avoids this problem by making mutable containers invariant, so the problem is caught at compile time rather than when the process is already running.
Type Safety and Runtime Behavior
Generic variance is a compile-time feature. The compiler rejects any use of a covariant type parameter in an input position and any use of a contravariant type parameter in an output position. The runtime performs no additional checks for generic variance, so there is no per-access cost beyond what the underlying type already incurs.
Array covariance is the exception. Because the compiler allows the conversion, the runtime must verify each write, which is why ArrayTypeMismatchException exists and why array writes carry a small per-element check that generic collections do not.
Variance applies only to reference types. Value types do not participate because the conversion would require boxing, and the CLR does not treat boxed conversions as reference conversions. IEnumerable<int> is not assignable to IEnumerable<object> through variance, even though int derives from object in the logical type hierarchy.
Choosing Between Covariance and Contravariance
The decision follows the shape of the type:
- Use
outwhen the type parameter appears only in output positions: read-only collections, factories, and repositories that return values. - Use
inwhen the type parameter appears only in input positions: comparers, predicates, and handlers that consume values. - Keep the type invariant when the type parameter appears in both positions. A mutable collection such as
List<T>must be invariant because it both accepts and returnsT.
A single type parameter cannot be both in and out. If a type needs both directions, split it into separate interfaces, as the framework does with IReadOnlyList<out T> and IList<T>. The read-only surface can be covariant; the mutable surface stays invariant.
Common Mistakes and Limitations
The most common mistake is declaring a covariant type parameter and then using it in a method parameter. The compiler rejects it:
public interface IProducer<out T> { void Add(T item); // error: T is used in an input position }
The same happens when a contravariant type parameter is used as a return type. The fix is to decide which direction the type genuinely needs and to split the interface if both are required.
Variance also does not apply to classes. Only interfaces and delegates can declare variant type parameters. A class such as List<T> cannot be marked out or in, which is consistent with its mutable nature.
Finally, variance is about reference conversions. It does not apply to value types, and it does not perform implicit user-defined conversions. An IEnumerable<Derived> converts to IEnumerable<Base> only when Derived and Base are related by inheritance, not when a custom implicit operator exists between them.