📁 Sangam Dhakal - Portfolio v2.0 - Netscape Navigator 4.0
_
□
×

📚 Interview question

What is covariance in C# generics? Explain the out modifier, why IEnumerable<string> converts to IEnumerable<object> but List<string> does not convert to List<object>, which BCL types are covariant, and the rules a covariant type parameter must follow.

Generic covariance: out T, output-only positions, reference conversions only, BCL examples and designing covariant interfaces and delegates.

Mid-Level Theory Advanced covariance csharp generics type-safety variance

🎯 Key points

  • Covariance preserves assignment direction: `IEnumerable<Derived>` is assignable to `IEnumerable<Base>`
  • Declared with `out T` on interfaces and delegates only, never on classes or structs
  • T may appear only in output positions (return types, out of getters), not as a method parameter
  • Works only for reference-type arguments: `IEnumerable<int>` is not `IEnumerable<object>`
  • Covariant BCL types: `IEnumerable<T>`, `IEnumerator<T>`, `IReadOnlyList<T>`, `IReadOnlyCollection<T>`, `IQueryable<T>`, `IGrouping<,>`, `Func<..., TResult>`
  • `List<T>` and `IList<T>` are invariant because T appears in inputs (Add)
  • Array covariance is older, unsafe at run time and different from generic covariance

⏱ 30-second answer

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>.

🗣 Full answer (senior)

How I would answer this in an interview

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.

1. The out modifier

public 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.

2. Why IEnumerable<string> converts but List<string> doesn't

  • IEnumerable<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>.

3. Covariant BCL types

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)

4. Rules a covariant type parameter must follow

  • Only on interfaces and delegates.
  • Only for reference-type arguments at the use site: IEnumerable<int> does not convert to IEnumerable<object> (would need boxing each element — a representation change).
  • The parameter may appear only in output-safe positions:
    • method return types,
    • get-only properties/indexers,
    • as a type argument to another covariant parameter (IEnumerable<T>),
    • as a type argument to a contravariant parameter of an input (e.g. parameter of type Action<T> — because input of an input is output).
  • It may not appear as: method parameter types (input), 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).

5. Practical use

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>.

6. A production example

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.

🔍 What the interviewer is checking

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.

📚 Learn in depth

1. How variance works at run time

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.

2. Variance validity rules (formal)

The compiler classifies positions as covariant (output), contravariant (input) or invariant:

  • A return type is covariant.
  • A parameter type is contravariant.
  • If 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.

3. Covariance and delegates

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.

4. Arrays: unsafe covariance

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.

5. Why Task<T> isn't covariant

Task<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.)

6. Designing covariant interfaces

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>.

Try it yourself

  1. Assign List<string> to IEnumerable<object> and to List<object>; read the error for the second.
  2. Try IEnumerable<int> to IEnumerable<object>.
  3. Declare interface IProducer<out T> { void Accept(T value); } and read CS1961.
  4. Split a store interface into read/write parts and use covariance on the read side.

💻 Code

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());
    }
}

⚠ Common mistakes

1. Expecting 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>.

2. Copying collections to change element type

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.

3. Expecting covariance with value types

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.

4. Declaring out T and using T as input

What 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.

5. Expecting class variance

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.

6. Confusing array covariance with generic covariance

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.

7. Expecting 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>.

8. Forgetting variance when designing interfaces

What happens: read-only interfaces declared without out. Why it is wrong: consumers lose flexibility. Do instead: add out to output-only type parameters.

🏛 Senior insight: trade-offs

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.

🔗 Follow-ups the interviewer may ask

  1. When: Candidate explains out T confidently. Why: Natural pair: the interviewer checks the candidate can reverse the direction correctly.
    How to answer

    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.

  2. When: Candidate says List<T> is invariant. Why: Tests applying variance knowledge to real compiler errors.
    How to answer

    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.

  3. When: Candidate mentions array covariance. Why: Contrasts safe generic covariance with unsafe array covariance.
    How to answer

    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.

🛠 Real-world problems: how you handle them

Month-end export copying huge lists to satisfy a parameter type
Situation
A finance reporting service exported 200k-row reports. Callers converted `List<SalesRow>` with `.Cast<ReportRow>().ToList()` because the exporter took `List<ReportRow>`.
Signals
Memory spikes and Gen 2 GCs at month-end, allocation profile showing large `ReportRow[]` arrays from ToList.
Investigation
Used dotnet-counters to watch GC heap size during an export and dotnet-trace allocation sampling, which pointed at the Cast/ToList call sites.
Resolution
Changed the exporter parameter to `IReadOnlyList<ReportRow>`. Because the interface is covariant, callers passed their lists directly; removed all the copies.
Prevention
Added a guideline to accept read-only interfaces in method parameters, and enabled an analyser rule against unnecessary ToList in the module.

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.

↩ Usually asked after

More in Generics

Last updated 30 Oct 2026.

🌐 Sangam's Webring

⏪ Previous 🏠 Main Next ⏩
💬 Chat with me