Generic covariance: out T, output-only positions, reference conversions only, BCL examples and designing covariant interfaces and delegates.
Covariance means a generic type keeps the direction of inheritance: because string is an object, IEnumerable<string> can be used as IEnumerable<object>. You declare it with out T, only on interfaces and delegates, and T may only be returned, never accepted. That is why IReadOnlyList<T> is covariant but List<T> is not: List<object>.Add(42) would break a list of strings. It only works with reference types, so IEnumerable<int> does not convert to IEnumerable<object>.
Covariance lets you use a generic type with a more derived type argument where a less derived one is expected: an IEnumerable<string> can be used as an IEnumerable<object>. It's marked with the out modifier and is only allowed when the type parameter appears in output positions, which is exactly what makes it type-safe.
out modifierpublic interface IReadOnlyRepository<out T>
{
T? Find(int id); // T in output position - OK
IEnumerable<T> GetAll(); // OK: IEnumerable<T> is itself covariant
// void Add(T item); // CS1961: T in input position - not allowed
}
IReadOnlyRepository<Dog> dogs = ...;
IReadOnlyRepository<Animal> animals = dogs; // allowed: every Dog is an Animal
If you can only get T out, then a source of Dogs is a perfectly good source of Animals.
IEnumerable<string> converts but List<string> doesn'tIEnumerable<out T> only produces Ts (via GetEnumerator().Current), so treating strings as objects can never break anything — reading a string as an object is always valid.List<T> is a class (variance is only allowed on interfaces and delegates) and it both reads and writes T. If List<string> could be treated as List<object>:List<object> objects = new List<string>(); // imagine this were allowed
objects.Add(42); // now a List<string> contains an int!
That would break type safety (the same problem that array covariance has — and arrays need a runtime check on every write because of it). So List<T> is invariant.
Use the covariant interfaces when you only need to read: IEnumerable<T>, IReadOnlyList<T>, IReadOnlyCollection<T>.
| Type | Covariant parameter |
|---|---|
IEnumerable<out T>, IEnumerator<out T> |
T |
IReadOnlyCollection<out T>, IReadOnlyList<out T> |
T |
IQueryable<out T>, IOrderedEnumerable<out T>, IGrouping<out TKey, out TElement> |
T / both |
IAsyncEnumerable<out T>, IAsyncEnumerator<out T> |
T |
Func<out TResult> (and the result of all Func<...>) |
TResult |
Lazy<T>? |
No — classes can't be variant |
Task<T> |
No — class, invariant |
IReadOnlyDictionary<TKey, TValue> |
No — TValue appears in TryGetValue(out TValue) |
IEnumerable<int> does not convert to IEnumerable<object> (would need boxing each element — a representation change).get-only properties/indexers,IEnumerable<T>),Action<T> — because input of an input is output).ref/out parameters (both directions), settable properties, generic constraints in certain positions, or invariant generic types like List<T>.The compiler checks these rules when you declare out T (CS1961).
void PrintNames(IEnumerable<IHasName> items) { ... }
List<Customer> customers = ...; // Customer : IHasName
PrintNames(customers); // works thanks to covariance (List<T> implements IEnumerable<T>)
This is why accepting IEnumerable<T>/IReadOnlyList<T> in APIs is more flexible than List<T>.
A reporting method was declared void Render(List<ReportRow> rows) and callers with List<SalesRow> (deriving from ReportRow) had to copy lists with .Cast<ReportRow>().ToList() — allocating large lists for every report. Changing the parameter to IReadOnlyList<ReportRow> allowed callers to pass their lists directly via covariance, removing the copies and simplifying the code.
Checks whether the candidate really understands variance or just remembers the keyword.
Green flags: explains why List<T> cannot be covariant with an Add example; knows the reference-type-only rule; names BCL covariant interfaces; mentions Task<T> is invariant.
Red flags: says "covariance means you can cast anything"; confuses out keyword on parameters with out T variance; does not know classes cannot be variant.
Variance is supported by the CLR, not just the compiler. An interface type IEnumerable<string> is a different type from IEnumerable<object>, but the runtime's casting logic knows IEnumerable<out T> is covariant and that string is a reference type assignable to object, so the cast succeeds with no conversion — the same object reference is reused. That's why it's restricted to reference types: a string reference is already a valid object reference; an int value isn't an object without boxing.
Variance checks make some casts slightly more expensive (the runtime has to consider variant compatibility), but the casting cache makes repeated casts fast.
The compiler classifies positions as covariant (output), contravariant (input) or invariant:
I<out U> is used in a covariant position, its argument position is covariant; if I<in U>, its argument position flips.ref/out parameters are invariant.An out T must appear only in covariant positions after this flipping. E.g. void Register(Action<T> callback) in IFoo<out T> is valid: Action<in T> flips the parameter position back to covariant.
Func<string> getString = () => "hi";
Func<object> getObject = getString; // covariant in TResult
Besides generic variance, C# also has method group variance: a method returning string can be assigned to a Func<object> delegate.
Arrays are covariant for reference types (string[] to object[]), but they're mutable — so the runtime checks every write (ArrayTypeMismatchException). Generic variance avoided that design mistake by allowing variance only where it's statically safe.
Task<T> isn't covariantTask<T> is a class, and class type parameters can't be variant in .NET. So Task<Dog> can't be returned where Task<Animal> is expected; you must await and return, or use ValueTask/Task.FromResult<Animal>. (There have been proposals for covariant tasks via interfaces like ITask<out T> in libraries.)
Split read and write interfaces so the read side can be covariant:
public interface IReadStore<out T> { T Get(int id); }
public interface IWriteStore<in T> { void Put(int id, T value); }
public interface IStore<T> : IReadStore<T>, IWriteStore<T> { }
Consumers that only read can accept IReadStore<Animal> and be given an IStore<Dog>.
List<string> to IEnumerable<object> and to List<object>; read the error for the second.IEnumerable<int> to IEnumerable<object>.interface IProducer<out T> { void Accept(T value); } and read CS1961.namespace Generics.Covariance;
public abstract record ReportRow(string Label);
public sealed record SalesRow(string Label, decimal Amount) : ReportRow(Label);
public interface IRowSource<out TRow> where TRow : ReportRow
{
TRow First();
IEnumerable<TRow> All();
}
public sealed class SalesSource(IReadOnlyList<SalesRow> rows) : IRowSource<SalesRow>
{
public SalesRow First() => rows[0];
public IEnumerable<SalesRow> All() => rows;
}
public static class Exporter
{
public static void Export(IReadOnlyList<ReportRow> rows)
{
foreach (var row in rows) Console.WriteLine(row.Label);
}
public static void ExportFrom(IRowSource<ReportRow> source)
{
foreach (var row in source.All()) Console.WriteLine($"source: {row.Label}");
}
}
public static class Program
{
public static void Main()
{
List<SalesRow> sales = [new("Jan", 100m), new("Feb", 150m)];
Exporter.Export(sales); // IReadOnlyList<SalesRow> -> IReadOnlyList<ReportRow>, no copy
Exporter.ExportFrom(new SalesSource(sales)); // IRowSource<SalesRow> -> IRowSource<ReportRow>
Func<SalesRow> makeSales = () => new SalesRow("Mar", 90m);
Func<ReportRow> makeRow = makeSales; // delegate covariance
Console.WriteLine(makeRow().Label);
IEnumerable<object> objects = sales; // reference types: OK
// IEnumerable<object> boxed = new List<int>(); // CS0266: value types are not variant
IEnumerable<object> boxed = new List<int> { 1, 2 }.Cast<object>();
Console.WriteLine(objects.Count() + boxed.Count());
}
}
List<Derived> to be a List<Base>What happens: passing List<Dog> to a List<Animal> parameter.
Why it is wrong: List<T> is invariant because it allows writes.
Do instead: accept IEnumerable<T> or IReadOnlyList<T>.
What happens: .Cast<Base>().ToList() just to satisfy a parameter type.
Why it is wrong: needless allocation and copying.
Do instead: change the parameter to a covariant interface.
What happens: IEnumerable<int> passed as IEnumerable<object>.
Why it is wrong: variance only applies to reference types.
Do instead: use .Cast<object>() (boxes) or make the method generic.
out T and using T as inputWhat happens: adding an Add(T item) method to a covariant interface.
Why it is wrong: compile error — it would break type safety.
Do instead: split read and write interfaces.
What happens: trying class Box<out T>.
Why it is wrong: only interfaces and delegates support variance.
Do instead: expose a covariant interface implemented by the class.
What happens: assuming arrays are as safe as IEnumerable<T>.
Why it is wrong: array covariance is unsafe and checked at run time on writes.
Do instead: use read-only generic interfaces for covariant usage.
Task<Derived> to be Task<Base>What happens: returning Task<Dog> from a method declared Task<Animal>.
Why it is wrong: Task<T> is an invariant class.
Do instead: return await GetDogAsync(); in an async method returning Task<Animal>.
What happens: read-only interfaces declared without out.
Why it is wrong: consumers lose flexibility.
Do instead: add out to output-only type parameters.
Covariance is mostly invisible when APIs are designed well, and painful when they are not. The practical lesson for seniors: accept the least capable interface you need (IEnumerable<T>, IReadOnlyList<T>), because those are covariant and spare callers from copying. When I design interfaces I separate producers (out) from consumers (in), which also gives cleaner responsibilities. I mention array covariance as a legacy trap and the cost of its run-time store check.
Contravariance is the opposite of covariance: a consumer of a base type can stand in for a consumer of a derived type. You declare it with in T on interfaces and delegates, and T may only be accepted, never returned. So an IComparer<object> can sort a List<string>, and an Action<Animal> can be used as an Action<Dog>. The BCL comparers, Action, Predicate and the parameters of Func are contravariant. One trap: combining variance-converted delegates with += can throw at run time because their actual types differ.
These all fail because the types are invariant: List<Dog> and List<Animal> are unrelated even though Dog is an Animal. Classes like List<T>, Dictionary<,> and Task<T> are always invariant, and IDictionary<,> is invariant because it reads and writes values. I fix it by accepting a covariant interface like IReadOnlyList<Animal>, by making the method generic with where T : Animal, or by projecting explicitly, for example awaiting the task and returning the result.
Reference-type arrays in .NET are covariant: a string[] can be treated as an object[]. The array still really holds strings, so the runtime checks every write, and storing 42 fails with ArrayTypeMismatchException. The feature exists for historical reasons (Java had it, and .NET 1.0 had no generics) and it makes array writes type-unsafe at compile time. It also adds a hidden type check to stores into arrays of non-sealed reference types, and new Span<object>(stringArray) throws. The safe alternative is generic variance: IEnumerable<out T> and IReadOnlyList<out T> are covariant because they only return values, while List<T> is invariant, so the compiler catches the mistake.
How to tell it (STAR)
**S:** Month-end exports were causing memory alarms on our reporting service. **T:** Reduce memory without rewriting the exporter. **A:** I found every caller was copying lists only to match a `List<ReportRow>` parameter, changed it to the covariant `IReadOnlyList<ReportRow>` and removed the copies. **R:** Peak memory dropped by [amount] and the alarms stopped.
Last updated 30 Oct 2026.