Senior 10 min · March 06, 2025

C# Variance — Why Array Store Checks Crash Production Loops

ArrayTypeMismatchException crashed a payment pipeline after array widening.

N
Naren · Founder
Plain-English first. Then code. Then the interview question.
About
 ● Production Incident 🔎 Debug Guide
Quick Answer
  • Covariance (out) lets a generic interface produce derived types; contravariance (in) lets it consume base types
  • IEnumerable and IComparer are the canonical BCL examples
  • Covariance: IProducer is assignable to IProducer; contravariance: IProcessor is assignable to IProcessor
  • Only interfaces and delegates support variance; classes and structs are always invariant
  • Array covariance is legacy and unsound — writes through a widened reference throw ArrayTypeMismatchException at runtime
  • Constructed generic delegate types are not directly castable even when variance applies — method group assignment is the safe path
  • The compiler fully verifies variance safety when you use in or out on type parameters
Plain-English First

Imagine you have a basket labeled 'Fruit'. You can put apples in it because an apple IS a fruit — that's covariance, flowing in the same direction as inheritance. Now imagine a machine that processes any fruit. You can use that machine to process apples too, even though it was built for fruit in general — that's contravariance, flowing in the opposite direction. Covariance lets you use a more specific type where a more general one is expected (producing values — reading them out). Contravariance lets you use a more general handler where a specific one is expected — because the handler can process any input of the broader type, it can certainly handle the narrower one too. Both are about making type substitution safe and predictable, not about bypassing the type system.

Generic type safety is one of the most quietly powerful features of C#, and covariance and contravariance sit right at its heart. Every time you assign an IEnumerable<string> to an IEnumerable<object>, or pass a Func<Animal> where a Func<Cat> is expected, the CLR is doing something subtle and deliberate on your behalf. Most developers use these features daily without knowing their names — and that gap in knowledge causes real production bugs.

The problem these concepts solve is deceptively simple: how do you make generic types work safely with inheritance hierarchies? Without covariance and contravariance, you'd have to write casting boilerplate everywhere, or worse, end up with runtime InvalidCastExceptions that slip past the compiler. The C# type system gives you a way to declare, at the interface or delegate level, exactly which direction type substitution is safe — and the compiler enforces it.

By the end of this article you'll understand exactly what the 'in' and 'out' keywords do on generic type parameters, why arrays in C# are covariant but carry a runtime cost, how delegate variance works for method group assignments and where it silently breaks for constructed generic delegate types, where variance is intentionally unsupported and why, and how to apply all of this in production code with confidence. You'll also be ready to answer the variance questions that trip up experienced developers in senior-level interviews.

The Type Substitution Problem — Why Variance Exists at All

Here's a question that sounds like it should have an obvious answer: if Dog inherits from Animal, why can't you assign a List<Dog> to a List<Animal>?

The Liskov Substitution Principle says a Dog can stand in anywhere an Animal is expected — that's the whole point of inheritance. So the assumption that List<Dog> IS-A List<Animal> feels natural. But it's wrong, and understanding exactly why is the doorway into variance.

If List<Dog> were assignable to List<Animal>, nothing would stop you from calling Add(new Cat()) on the widened reference. The compiler sees List<Animal> and allows it. The runtime sees a List<Dog> and explodes. That's not a type system — that's a landmine. This is why List<T> is invariant: no substitution in either direction is permitted, full stop.

Variance solves this by being surgical rather than wholesale. Instead of making List<T> magically accept subtypes, C# lets interface and delegate designers mark individual type parameters as safe for covariant use (out — you only return T, never accept it) or safe for contravariant use (in — you only accept T, never return it). The compiler then verifies those contracts are upheld everywhere inside the type, making the variance provably safe at compile time, not a runtime gamble.

This is the key insight most articles skip: variance isn't magic permissiveness. It's a compile-time-verified contract about data flow direction. The 'out' keyword is a promise that T only exits the type — like water flowing out of a pipe. The 'in' keyword is a promise that T only enters the type — like water flowing in. If data needs to flow both ways, the pipe must be invariant, full stop.

VarianceMotivation.csCSHARP
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
namespace io.thecodeforge.covariance;

using System;
using System.Collections.Generic;

public class Animal
{
    public string Name { get; init; }
    public Animal(string name) => Name = name;
    public override string ToString() => $"Animal({Name})";
}

public class Dog : Animal
{
    public Dog(string name) : base(name) { }
    public override string ToString() => $"Dog({Name})";
}

public class Cat : Animal
{
    public Cat(string name) : base(name) { }
    public override string ToString() => $"Cat({Name})";
}

class VarianceMotivation
{
    static void Main()
    {
        // --- WHY List<Dog> cannot be List<Animal> ---
        List<Dog> dogPack = new() { new Dog("Rex"), new Dog("Buddy") };

        // This line does NOT compile — List<T> is INVARIANT.
        // If it did compile, the next line would corrupt the list at runtime.
        // List<Animal> animals = dogPack;  // CS0029 — cannot implicitly convert
        // animals.Add(new Cat("Whiskers")); // Rex and Buddy would be very upset

        // --- BUT IEnumerable<Dog> CAN be used as IEnumerable<Animal> ---
        // IEnumerable<T> is COVARIANT (out T), so this is safe and legal.
        // We can only READ from IEnumerable — we can never Add to it.
        // Therefore the Cat-corruption scenario is impossible.
        IEnumerable<Animal> safeAnimals = dogPack; // compiles just fine

        Console.WriteLine("Safe covariant assignment succeeded.");
        foreach (Animal animal in safeAnimals)
        {
            // Each element is still a Dog at runtime — covariance preserves identity.
            // The variable type is Animal, but GetType() proves the real type survived.
            Console.WriteLine($"  {animal} — runtime type: {animal.GetType().Name}");
        }
    }
}
Output
Safe covariant assignment succeeded.
Dog(Rex) — runtime type: Dog
Dog(Buddy) — runtime type: Dog
The Direction Rule:
Think of 'out' as a read-only outlet — data flows OUT of the type to the caller, so widening is safe. Think of 'in' as a write-only inlet — data flows IN from the caller, so widening to a more general handler is safe. If data flows in both directions, the type parameter must be invariant. When in doubt, sketch the arrow: which way does T move at the call site? That arrow tells you the keyword.
Production Insight
When you design a library interface, always default to invariant. Only add out or in after you have confirmed the usage pattern is genuinely read-only or write-only. Premature variance closes the door on future API additions — if you later need to add a method that goes in the opposite direction, removing in or out is a breaking change for every caller. Pay the cost of that decision consciously, not by accident.
Key Takeaway
Variance is a compile-time contract, not a runtime feature. If you need both read and write on the same type parameter, keep T invariant. Fighting the compiler on this is always the wrong call.

Covariance With 'out' — Building and Using Covariant Interfaces

The most common question after understanding the type substitution problem is: 'okay, but how do I actually get LINQ and IEnumerable to work across my inheritance hierarchy without casting everywhere?' That's covariance, and it's declared with the 'out' keyword.

Once you mark a type parameter as 'out', the compiler enforces one strict rule: T can only appear in output positions — return types, property getters, and out parameters. It cannot appear as a method parameter type. This restriction is what makes the covariant assignment safe. The compiler verifies this every time you implement the interface too — if you try to put 'out T' in a method parameter, you'll get CS1961 immediately.

The canonical example in the BCL is IEnumerable<out T>. Because the interface only ever produces T values (via MoveNext/Current), it's provably impossible to inject a wrong-typed object through it. This is why every LINQ extension method that takes IEnumerable<T> works with IEnumerable<Dog>, IEnumerable<string>, and so on — covariance is doing that work silently in the background every time.

Where this matters in real code: factory results, read-only projections, producer patterns. Any time you have an interface that returns objects of type T but never accepts T as input, mark that parameter as 'out' and you unlock free assignment compatibility across your entire inheritance hierarchy without a single cast.

One nuance worth burning into memory: covariance only works on interfaces and delegates, never on classes. List<Dog> will never be assignable to List<Animal> regardless of what you do, because List<T> is a class. This is by design — classes have mutable state that makes variance unsound. If you want variance on a class, extract an interface and put the 'out' keyword there. The class stays invariant; the interface carries the variance contract.

Another subtlety: a covariant type parameter inside a covariant wrapper is still covariant, but a covariant type parameter inside a contravariant wrapper flips to contravariant. IEnumerable<out T> inside a return position is fine; IEnumerable<out T> inside a method parameter position would break covariance. The compiler tracks this chain automatically and will tell you exactly where the violation is.

CovariantProducer.csCSHARP
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
namespace io.thecodeforge.covariance;

using System;
using System.Collections.Generic;

// --- Covariant interface: T only flows OUT ---
// The 'out' keyword tells the compiler: this interface only produces T values.
// It can never accept a T as an argument, so widening is provably safe.
public interface IAnimalProducer<out TAnimal> where TAnimal : Animal
{
    TAnimal Produce();                           // Legal: T in return position (output)
    IEnumerable<TAnimal> ProduceBatch(int count); // Legal: T inside a covariant wrapper
    // void Accept(TAnimal animal);              // ILLEGAL — CS1961, T in input position
}

public class DogBreeder : IAnimalProducer<Dog>
{
    private readonly string[] _names = { "Apollo", "Bella", "Caesar" };
    private int _index = 0;

    public Dog Produce() => new Dog(_names[_index++ % _names.Length]);

    public IEnumerable<Dog> ProduceBatch(int count)
    {
        for (int i = 0; i < count; i++)
            yield return Produce();
    }
}

public class RescueShelter : IAnimalProducer<Cat>
{
    public Cat Produce() => new Cat("Rescue-" + Guid.NewGuid().ToString()[..4]);

    public IEnumerable<Cat> ProduceBatch(int count)
    {
        for (int i = 0; i < count; i++)
            yield return Produce();
    }
}

class CovariantProducer
{
    // This method accepts any IAnimalProducer<Animal>.
    // Because of covariance, we can pass an IAnimalProducer<Dog> or IAnimalProducer<Cat>.
    static void DisplayThreeAnimals(IAnimalProducer<Animal> producer)
    {
        Console.WriteLine($"Producer type: {producer.GetType().Name}");
        foreach (Animal animal in producer.ProduceBatch(3))
        {
            // Runtime type is preserved — covariance does not erase the concrete type.
            // The variable is typed as Animal, but GetType() returns the real class.
            Console.WriteLine($"  Got: {animal} [{animal.GetType().Name}]");
        }
    }

    static void Main()
    {
        IAnimalProducer<Dog> dogBreeder = new DogBreeder();
        IAnimalProducer<Cat> rescueShelter = new RescueShelter();

        // Covariant assignment: IAnimalProducer<Dog> → IAnimalProducer<Animal>
        // This compiles ONLY because TAnimal is marked 'out'.
        IAnimalProducer<Animal> animalSource = dogBreeder;

        DisplayThreeAnimals(dogBreeder);     // passes directly
        DisplayThreeAnimals(rescueShelter);  // Cat producer accepted as Animal producer
        DisplayThreeAnimals(animalSource);   // the explicitly widened reference
    }
}
Output
Producer type: DogBreeder
Got: Dog(Apollo) [Dog]
Got: Dog(Bella) [Dog]
Got: Dog(Caesar) [Dog]
Producer type: RescueShelter
Got: Cat(Rescue-a3f1) [Cat]
Got: Cat(Rescue-b72c) [Cat]
Got: Cat(Rescue-e9d4) [Cat]
Producer type: DogBreeder
Got: Dog(Apollo) [Dog]
Got: Dog(Bella) [Dog]
Got: Dog(Caesar) [Dog]
Pro Tip — LINQ relies on this:
Every LINQ extension method that takes IEnumerable<T> works with IEnumerable<Dog>, IEnumerable<string>, and so on, partly because of IEnumerable<out T>'s covariance. When you design a read-only producer interface in your own codebase, add 'out' to the type parameter — you'll save every caller a cast, make the API composable with LINQ for free, and signal clearly to anyone reading the interface that T only ever comes out of it.
Production Insight
Marking a type parameter 'out' is a public promise that T will never appear in input positions. If you later need to add an Add(T item) method to that interface, you must remove 'out' — and that is a breaking change for every caller who relied on the covariant assignment. Design for variance deliberately. If you are not certain the interface will remain purely a producer, leave it invariant and revisit later. Adding 'out' is cheap; removing it is painful.
Key Takeaway
Covariance flows WITH inheritance: IProducer<Dog> is assignable to IProducer<Animal>. Only use 'out' on pure producer interfaces. The moment you need to accept T as input, the covariant contract is broken and CS1961 will tell you immediately.

Contravariance With 'in' — When a General Handler Beats a Specific One

Contravariance is the one that makes developers pause and re-read the line three times. Not because it's complicated — once it clicks, it's obvious — but because the assignment direction is backwards from everything inheritance has trained you to expect.

Here's the scenario that makes it concrete: you have an IComparer<Animal> that compares any two animals by name. You need to sort a List<Dog>. List<Dog>.Sort() expects an IComparer<Dog>. Do you need a separate DogComparer? No — and if you've ever written one when you already had a working AnimalComparer, this section is for you.

The 'in' keyword on a type parameter means T can only appear in input positions — method parameters and property setters. It cannot appear in return types. This makes the assignment direction flip: a more general type can be assigned to a more specific one. An IComparer<Animal> IS assignable to IComparer<Dog>. The reason is mechanical and worth saying once clearly: if a handler can process any Animal, it can certainly process a Dog, because Dog IS an Animal. The handler doesn't know or care that it's receiving something more specific — it already handles the more general case.

Where this matters in production: event handlers, comparers, validators, formatters, loggers — anything that consumes a value rather than producing it. If your interface only ever accepts T as input and never returns it, mark T as 'in' and you get assignment compatibility in the useful direction: callers can supply a broader handler and it just works.

The real power shows up when you combine covariance and contravariance in the same pipeline. A function that accepts a broad input type and returns a narrow output type composes beautifully across inheritance boundaries — which is exactly what Func<in TInput, out TOutput> expresses in the BCL. This is not a coincidence; the C# team designed Func and Action this way precisely to support real-world composition patterns.

ContravariantProcessor.csCSHARP
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
namespace io.thecodeforge.covariance;

using System;
using System.Collections.Generic;

// --- Contravariant interface: T only flows IN ---
// The 'in' keyword means this interface only CONSUMES T values.
// Therefore a handler of Animal can safely act as a handler of Dog.
public interface IAnimalProcessor<in TAnimal> where TAnimal : Animal
{
    void Process(TAnimal animal);                  // Legal: T in parameter (input position)
    void ProcessBatch(IEnumerable<TAnimal> batch); // Legal: T inside an input parameter
    // TAnimal Retrieve();                         // ILLEGAL — CS1962, T in output position
}

// A general processor that handles ANY animal
public class AnimalHealthChecker : IAnimalProcessor<Animal>
{
    public void Process(Animal animal)
    {
        // Works on any Animal — so it works on Dog and Cat too.
        // The concrete type is visible at runtime even through the contravariant interface.
        Console.WriteLine($"  Health check passed for {animal} [{animal.GetType().Name}]");
    }

    public void ProcessBatch(IEnumerable<Animal> batch)
    {
        foreach (Animal animal in batch)
            Process(animal);
    }
}

// A specific processor that only handles Dogs
public class DogTrainer : IAnimalProcessor<Dog>
{
    public void Process(Dog dog)
    {
        Console.WriteLine($"  Training session for {dog}");
    }

    public void ProcessBatch(IEnumerable<Dog> batch)
    {
        foreach (Dog dog in batch)
            Process(dog);
    }
}

class ContravariantProcessor
{
    // This method expects something that processes Dogs specifically.
    // Because IAnimalProcessor<in TAnimal> is contravariant, the general
    // AnimalHealthChecker is accepted here — it handles any Animal, so Dogs are fine.
    static void RunDogPipeline(IAnimalProcessor<Dog> dogProcessor, IEnumerable<Dog> dogs)
    {
        Console.WriteLine($"  Using processor: {dogProcessor.GetType().Name}");
        dogProcessor.ProcessBatch(dogs);
    }

    static void Main()
    {
        var dogPack = new List<Dog> { new Dog("Max"), new Dog("Luna") };

        IAnimalProcessor<Animal> generalChecker = new AnimalHealthChecker();
        IAnimalProcessor<Dog> specificTrainer = new DogTrainer();

        // Contravariant assignment: IAnimalProcessor<Animal> → IAnimalProcessor<Dog>
        // This flows OPPOSITE to inheritance: Animal is broader than Dog,
        // yet the Animal processor is assignable to the Dog processor slot.
        // Safe because: anything a Dog processor is asked to handle IS an Animal.
        IAnimalProcessor<Dog> checkerAsDogProcessor = generalChecker; // compiles cleanly

        Console.WriteLine("Running with specific DogTrainer:");
        RunDogPipeline(specificTrainer, dogPack);

        Console.WriteLine("Running with general AnimalHealthChecker (contravariant):");
        RunDogPipeline(generalChecker, dogPack);        // general processor passed directly
        RunDogPipeline(checkerAsDogProcessor, dogPack); // explicit contravariant reference
    }
}
Output
Running with specific DogTrainer:
Using processor: DogTrainer
Training session for Dog(Max)
Training session for Dog(Luna)
Running with general AnimalHealthChecker (contravariant):
Using processor: AnimalHealthChecker
Health check passed for Dog(Max) [Dog]
Health check passed for Dog(Luna) [Dog]
Running with general AnimalHealthChecker (contravariant):
Using processor: AnimalHealthChecker
Health check passed for Dog(Max) [Dog]
Health check passed for Dog(Luna) [Dog]
Watch Out — Contravariance Reverses Your Intuition:
With contravariance, the assignment goes against the inheritance arrow: IProcessor<Animal> is assignable to IProcessor<Dog>, even though Dog derives from Animal. This is correct and intentional. If you find yourself fighting the compiler on this, sketch the data flow: ask 'does T come IN or go OUT at the call site?' and the right keyword will follow. The mnemonic that sticks: producers widen output upward (covariance), consumers widen input downward (contravariance).
Production Insight
Contravariance is excellent for dependency injection containers — you can register a handler for the base type and have it resolve correctly for all derived types without separate registrations. This pattern is widely used in message bus and event dispatcher implementations. But be careful: if the handler ever needs to return T — even just for chaining — contravariance breaks and CS1962 fires. Keep consumers and producers on separate interfaces from the start.
Key Takeaway
Contravariance flows AGAINST inheritance: IProcessor<Animal> is assignable to IProcessor<Dog>. Only use 'in' on pure consumer interfaces. The moment you need to return T, the contravariant contract is broken and CS1962 will tell you immediately.

Delegate Variance, Array Covariance, and the Hidden Runtime Cost

Two features in this section look like variance but behave very differently from each other — and from the interface variance you've seen so far. Getting them confused is how production incidents happen.

Delegates in C# support variance for method group assignments without any in or out annotation. A method that returns a Dog can be assigned to a Func<Animal> delegate variable (covariance). A method that accepts an Animal can be assigned to an Action<Dog> delegate variable (contravariance). The compiler infers compatibility from the method signature directly.

Here is the part that catches experienced engineers off guard: this variance applies to method group assignments specifically, not to delegate instance casting. You cannot cast a Func<Dog> instance directly to Func<Animal> and expect it to work. The compiler may allow the cast syntactically in some contexts, but the CLR will throw InvalidCastException at runtime because the underlying delegate types are different constructed generic types — Func<Dog> and Func<Animal> have no inheritance relationship between them, variance-compatible or not. The safe path is always method group assignment, or wrapping: Func<Animal> f = () => existingDogFunc(). Burn this into your team's coding standards. It surprises engineers with years of C# experience.

Array covariance is a different and older story — and a problematic one. string[] is assignable to object[] in C# because arrays have been covariant since C# 1.0, predating generics. This was a pragmatic decision (Java made the same one), but it is unsound: you can store any object reference in the object[] variable and the compiler will not stop you. The runtime catches it with an ArrayTypeMismatchException, but that is a runtime failure, not a compile-time one — exactly the kind of bug you want the type system to prevent.

The key difference from IEnumerable<out T> is that arrays are mutable. You can write to an array through the widened reference, which is what creates the danger. IEnumerable<out T> avoids this by being read-only by design. This is why sound generic covariance only works on interfaces and delegates, not on classes or arrays.

The performance angle deserves its own paragraph: every write to an array that was assigned to a wider element-type variable goes through a CLR runtime check called the covariant array store check. This is a type identity comparison that happens on every write, not just the ones that could be problematic. In a tight loop writing millions of elements, this overhead is measurable. For performance-sensitive paths, the right tools are strongly typed arrays or Span<T> — but note that Span<T> is a ref struct and cannot participate in generic variance at all. It cannot be used as a type argument to IReadOnlyList<T> or any other generic interface. Its job is stack-allocated, high-performance buffer access, not covariant abstraction. For covariant public APIs, IReadOnlyList<out T> is the correct choice. Here's the pattern in one place:

```csharp // BEFORE: covariant array — unsafe for writes, store-check overhead on every write Dog[] dogs = GetDogs(); Animal[] animals = dogs; // compiles, dangerous

// AFTER: safe covariant view — no writes possible, no store check, clear API intent IReadOnlyList<Animal> animals = new List<Animal>(dogs); // explicit, honest, safe // or, if you genuinely only need a read-only view of the existing array: IReadOnlyList<Animal> view = dogs; // IReadOnlyList<out T> is covariant — this is sound ```

The second form — assigning Dog[] directly to IReadOnlyList<Animal> — works because IReadOnlyList<out T> is covariant and the interface prevents writes. No store check, no exception risk, and the read-only contract is enforced at compile time by the interface itself.

DelegateAndArrayVariance.csCSHARP
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
namespace io.thecodeforge.covariance;

using System;

class DelegateAndArrayVariance
{
    // --- Method group covariance ---
    // Returns Dog (more specific), compatible with Func<Animal> (more general).
    // This works because Dog IS-AN Animal — covariance flows with inheritance.
    static Dog CreateDog() => new Dog("Scout");

    // --- Method group contravariance ---
    // Accepts Animal (more general), compatible with Action<Dog> (more specific).
    // This works because Dog IS-AN Animal — the method can handle any Animal,
    // so it can certainly handle a Dog.
    static void LogAnimal(Animal animal)
        => Console.WriteLine($"  [LOG] {animal} ({animal.GetType().Name})");

    static void Main()
    {
        // DELEGATE COVARIANCE — method group assignment
        // CreateDog returns Dog; Dog IS-AN Animal; so Func<Animal> can hold it.
        Func<Animal> animalFactory = CreateDog;
        Animal produced = animalFactory();
        Console.WriteLine($"Delegate covariance produced: {produced}");

        // DELEGATE CONTRAVARIANCE — method group assignment
        // LogAnimal accepts Animal; Dog IS-AN Animal; so a method handling any Animal
        // can handle Dogs specifically. The assignment goes against inheritance direction.
        Action<Dog> dogLogger = LogAnimal;
        dogLogger(new Dog("Ranger"));

        // THE DELEGATE VARIANCE TRAP — do not cast delegate instances directly
        Func<Dog> dogFactory = CreateDog;
        // Func<Animal> wrongWay = (Func<Animal>)dogFactory; // InvalidCastException at runtime!
        // The compiler may not always catch this. The CLR will.
        // The correct approach is to wrap the existing delegate:
        Func<Animal> rightWay = () => dogFactory();
        Console.WriteLine($"Wrapped delegate covariance: {rightWay()}");

        // --- ARRAY COVARIANCE — legal but dangerous ---
        Dog[] dogArray = { new Dog("Fido"), new Dog("Spot") };

        // Compiles fine — arrays have been covariant since C# 1.0.
        // animalArray and dogArray point to the SAME memory.
        Animal[] animalArray = dogArray;

        Console.WriteLine("\nReading through widened array reference (safe):");
        foreach (Animal a in animalArray)
            Console.WriteLine($"  {a}");

        // WRITING through the widened reference — runtime ArrayTypeMismatchException!
        // The compiler sees Animal[] and allows the assignment syntactically.
        // The CLR sees Dog[] at runtime and rejects Cat — covariant array store check.
        Console.WriteLine("\nAttempting unsafe array write...");
        try
        {
            animalArray[0] = new Cat("Mittens"); // ArrayTypeMismatchException here
        }
        catch (ArrayTypeMismatchException ex)
        {
            Console.WriteLine($"  Runtime caught it: {ex.GetType().Name}");
            Console.WriteLine("  The CLR store check fires on every write, not just bad ones.");
            Console.WriteLine("  In a tight loop this overhead is measurable.");
        }

        // The safe alternative: IReadOnlyList<out T> is genuinely covariant
        // and prevents write access through the interface entirely.
        // No store check, no exception risk, no performance overhead after construction.
        // Note: Span<T> is NOT usable here — it's a ref struct and cannot be a generic
        // type argument. Use Span<T> for stack-allocated buffer performance, not for
        // covariant abstractions.
        System.Collections.Generic.IReadOnlyList<Animal> safeView = dogArray;
        Console.WriteLine($"\nSafe covariant view via IReadOnlyList: {safeView[0]}");
        // safeView[0] = new Cat("Mittens"); // CS0200 — property or indexer is read-only
    }
}
Output
Delegate covariance produced: Dog(Scout)
[LOG] Dog(Ranger) (Dog)
Wrapped delegate covariance: Dog(Scout)
Reading through widened array reference (safe):
Dog(Fido)
Dog(Spot)
Attempting unsafe array write...
Runtime caught it: ArrayTypeMismatchException
The CLR store check fires on every write, not just bad ones.
In a tight loop this overhead is measurable.
Safe covariant view via IReadOnlyList: Dog(Fido)
Production Gotcha — Two Traps in One Section:
First: never cast delegate instances between constructed generic types (Func<Dog> to Func<Animal>) — use method group assignment or wrapping instead. The compiler does not always catch the cast, but the CLR will throw at runtime. Second: every element write through a covariant array reference triggers a CLR store check — a cost you pay even when no exception occurs. In a tight loop processing millions of items this adds up. Switch to strongly typed arrays for performance-critical internal paths. For covariant public APIs, IReadOnlyList<T> is the right choice — not Span<T>, which is a ref struct and cannot participate in generic variance.
Production Insight
Array covariance is the number one source of runtime type errors in legacy C# codebases. If you see ArrayTypeMismatchException in production, trace back to where an array was upcast to a wider element type. The fix is almost always to replace the array with IReadOnlyList<T> for read-only public APIs and List<T> for mutable internal collections. Treat any array parameter in a new public API as a code smell — IReadOnlyList<T> is almost always the better choice.
Key Takeaway
Delegate variance is automatic for method group assignments — never cast delegate instances directly between constructed types. Array covariance is unsafe for writes and carries a runtime cost even when safe — prefer IReadOnlyList<T> for covariant public APIs. Span<T> is for performance-critical buffer access, not generic variance.

Common Compiler Errors and How to Fix Them — CS1960, CS1961, CS1962

Variance modifiers trigger specific compiler errors when misapplied. Learning to recognise and fix them immediately saves hours of investigation. All three are compile-time errors — the compiler is doing exactly its job, and the right response is always to fix the design, not suppress the error.

CS1960 occurs when you apply in or out to a type parameter on a class or struct. Variance is only allowed on interfaces and delegates. The fix is to extract an interface, declare the variance modifier there, and leave the concrete class invariant. The class can implement the interface without issue.

CS1961 fires when you use a covariant type parameter (out T) in an input position — for example, as a method parameter of type T inside the interface. The compiler is telling you that allowing T to flow in would make the covariant assignment unsafe: a caller could pass in a more derived type than the internal implementation expects. The fix is to remove the input-position usage of T, or to accept that the interface cannot be covariant and remove out.

CS1962 is the mirror: using a contravariant type parameter (in T) in an output position — like returning T from a method. That would let the consumer treat the result as a more specific type than it actually is, breaking type safety in the opposite direction. The fix is the same: remove the output-position usage of T, or remove in.

A subtler form of CS1961 and CS1962 occurs through chains of generic types. A covariant type parameter inside a contravariant wrapper flips direction. For example, if you have interface IProcessor<in T> and you try to use Func<T> as a return type, the compiler will flag it — Func<T> is covariant in T, and using a covariant position inside a contravariant interface violates the in/out contract. The error message will point to the chain, not just the leaf type. Read it carefully and follow the variance direction from the outermost type inward.

All of these errors prevent a category of runtime failures that would be nearly impossible to debug in production. Never suppress them.

VarianceErrors.csCSHARP
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
namespace io.thecodeforge.varianceerrors;

using System;

// Shared types for this file — kept in a separate namespace to avoid
// collision with the Animal/Dog/Cat types in other examples.
public class Shape
{
    public string Kind { get; init; }
    public Shape(string kind) => Kind = kind;
    public override string ToString() => $"Shape({Kind})";
}

public class Circle : Shape
{
    public double Radius { get; init; }
    public Circle(double radius) : base("Circle") => Radius = radius;
    public override string ToString() => $"Circle(r={Radius})";
}

// CS1960 — variance keyword on a CLASS (not allowed)
// public class BadClass<out T> { }  // error CS1960

// Correct: declare variance on an INTERFACE, implement with an invariant class
public interface IShapeProducer<out TShape> where TShape : Shape
{
    TShape Produce();                 // Legal: T in return position
    // void Accept(TShape s);        // ILLEGAL — would cause CS1961
}

public class CircleFactory : IShapeProducer<Circle>
{
    private readonly double _radius;
    public CircleFactory(double radius) => _radius = radius;
    public Circle Produce() => new Circle(_radius);
}

// CS1961 — 'out' type parameter used in input position
// interface IBrokenCovariant<out T>
// {
//     void Set(T value); // error CS1961 — T in parameter, violates 'out'
// }

// CS1962 — 'in' type parameter used in output position
// interface IBrokenContravariant<in T>
// {
//     T Get(); // error CS1962 — T in return, violates 'in'
// }

// CS1961 via generic chain — covariant T inside a contravariant wrapper flips direction
// interface IProcessorChain<in T>
// {
//     Func<T> GetProducer(); // error CS1961 — Func<T> is covariant in T,
//                            // so T appears in covariant position inside a contravariant interface
// }

class VarianceErrors
{
    static void Main()
    {
        // Covariant interface: CircleFactory produces Circle,
        // which is assignable to IShapeProducer<Shape> because TShape is 'out'
        IShapeProducer<Circle> circleFactory = new CircleFactory(5.0);
        IShapeProducer<Shape> shapeFactory = circleFactory; // covariant assignment

        Shape produced = shapeFactory.Produce();
        Console.WriteLine($"Produced via covariant interface: {produced}");
        Console.WriteLine($"Runtime type preserved: {produced.GetType().Name}");

        // Delegate variance — covariance via method group
        Func<Circle> circleFunc = () => new Circle(3.14);
        Func<Shape> shapeFunc = circleFunc.Method.CreateDelegate<Func<Shape>>(circleFunc.Target);
        // Simpler equivalent when you control the source:
        // Func<Shape> shapeFunc = () => circleFunc();
        Console.WriteLine($"Delegate covariance: {shapeFunc()}");

        // Delegate contravariance — method group assignment
        Action<Shape> shapeLogger = s => Console.WriteLine($"  [LOG] {s}");
        Action<Circle> circleLogger = shapeLogger; // contravariant: Action<in T>
        circleLogger(new Circle(2.71));
    }
}
Output
Produced via covariant interface: Circle(r=5)
Runtime type preserved: Circle
Delegate covariance: Circle(r=3.14)
[LOG] Circle(r=2.71)
Quick Compilation Error Reference:
CS1960: Variance modifier on a class or struct — extract an interface. CS1961: 'out' T used as a method parameter — remove the input usage or remove 'out'. CS1962: 'in' T used as a return type — remove the output usage or remove 'in'. Generic chain violations: a covariant T inside a contravariant wrapper (or vice versa) flips the required direction — follow the error message from the outermost type inward and trace the variance direction at each layer.
Production Insight
These errors are your friend — they prevent runtime corruption that would surface as InvalidCastException or silent data corruption in production, often in a code path that runs only under specific load conditions. If you see CS1960, you are trying to force variance onto a class — rethink the abstraction. If you see CS1961 or CS1962, the interface is trying to do too much in both directions — split it. Never suppress these errors with pragmas or casts. The runtime alternative is always worse.
Key Takeaway
CS1960, CS1961, and CS1962 are compile-time safety nets, not obstacles. Fix them by respecting data flow direction. If you cannot respect data flow direction on a given interface, the interface should be invariant.
● Production incidentPOST-MORTEMseverity: high

Array Covariance Bringing Down a Payment Processing Pipeline

Symptom
After deploying a performance optimisation that used a shared object[] buffer for different transaction types, the batch processor threw an unhandled ArrayTypeMismatchException during peak hours. Transactions were lost and the entire pipeline stalled. The stack trace pointed to an innocuous-looking array element assignment inside a hot loop — the kind of line nobody looks twice at during code review.
Assumption
The team assumed that because Transaction and RefundTransaction both inherit from FinancialEvent, an array of FinancialEvent could safely hold both types interchangeably. They further assumed that using a single FinancialEvent[] buffer would reduce allocations — it did, but at the cost of type safety. The refactoring passed code review because the compiler raised no objection. Everyone assumed silence meant safety.
Root cause
The buffer was created as Transaction[] and then upcast to FinancialEvent[] to allow storing both types in a single pass. A downstream processor attempted to write a RefundTransaction into what was actually a Transaction[] reference at runtime. The CLR's covariant array store check — a type identity comparison that happens on every array write through a widened reference — detected the mismatch and threw the exception. The check is silent overhead on every write, even the safe ones. In a high-throughput loop, this had been silently degrading performance for months before it became a correctness failure.
Fix
Replaced the covariant array with an IReadOnlyList<FinancialEvent> backed by a List<FinancialEvent>. The buffer became read-only after construction, eliminating the write path entirely. No runtime store check, no allocation overhead beyond the initial list construction, and no category of bug that can silently corrupt transaction records. The fix was three lines of change and should have been the original design.
Key lesson
  • Never widen an array reference when writes can occur — use strongly typed generic collections instead.
  • The CLR covariant store check fires on every write through a widened array reference, not just the bad ones. In tight loops this is measurable overhead even when no exception is ever thrown.
  • IReadOnlyList<out T> is safe for covariant reads; arrays are safe only when you guarantee no writes through the widened reference — a guarantee that is impossible to enforce at the call site.
  • Compiler silence is not type safety. Array covariance is a hole in the type system that the compiler deliberately does not close. Treat it as a deprecated pattern in any new code you write.
Production debug guideIdentify and fix the four most common variance bugs: array store checks, delegate type mismatches, cross-assembly visibility failures, and constructed generic delegate casting.4 entries
Symptom · 01
ArrayTypeMismatchException on a field assignment
Fix
1. Locate the line where the exception is thrown — it will always be an array element write, never a read. 2. Check if the array was assigned from a more derived element type (e.g., Transaction[] assigned to FinancialEvent[]). 3. Find where the write occurs through the wider reference — often several call frames away from where the array was created. 4. Replace the array with IReadOnlyList<T> for read-only access or a typed List<T> for mutable access. 5. If runtime write flexibility is truly needed, use List<object> explicitly and accept the loss of type inference.
Symptom · 02
CS1961 or CS1962 compile-time variance error
Fix
1. Check the type parameter marked with in or out. 2. For CS1961 (out parameter in input position): remove the method parameter that accepts T, or change the interface design so reading and writing are on separate interfaces. 3. For CS1962 (in parameter in output position): remove the return type that produces T, or make the interface invariant by removing in. 4. If both reading and writing are genuinely needed, the type parameter must be invariant — remove in/out entirely and accept that the interface will not support variance. This is the correct answer, not a workaround.
Symptom · 03
Delegate covariance does not work across assembly boundaries
Fix
1. Verify that the delegate type is compatible at the method group level — method return type is assignable upward (covariance), parameter types are assignable downward (contravariance). 2. If using Func<T> or Action<T> across assemblies, ensure the types are accessible (public) and the inheritance chain is intact in both assemblies. 3. Check for internal type visibility — covariance across assemblies requires public types on both sides of the assignment. 4. If still failing, assign via method group rather than delegate-to-delegate cast: new Func<BaseType>(existingMethod) instead of (Func<BaseType>)existingDelegate.
Symptom · 04
InvalidCastException when casting between constructed generic delegate types
Fix
1. Recognise the pattern: you have a Func<Dog> and are attempting to cast it directly to Func<Animal>. This will throw InvalidCastException at runtime even though the types are variance-compatible. 2. The fix is always to use method group assignment, never a direct cast between delegate instances: assign via Func<Animal> animalFunc = dogFuncMethod where dogFuncMethod is the underlying method, not an existing delegate variable. 3. If you only have a delegate instance and not the method, wrap it: Func<Animal> animalFunc = () => existingDogFunc(). 4. Document this pattern in your team's coding standards — it surprises even experienced engineers.
★ Quick Debug Cheat Sheet for Variance IssuesFour commands to diagnose variance-related runtime errors in C#. PowerShell syntax is used for Windows; bash equivalents are provided for Linux and macOS.
ArrayTypeMismatchException at runtime
Immediate action
Catch the exception and inspect the actual runtime types of the array and the element being assigned — the mismatch will be in GetType().GetElementType() vs the object's GetType()
Commands
PowerShell: dotnet run --project <project> 2>&1 | Select-String -Pattern "ArrayTypeMismatchException" | bash: dotnet run --project <project> 2>&1 | grep "ArrayTypeMismatchException"
In Visual Studio Immediate Window: `? array.GetType().GetElementType()` — compare the result against the runtime type of the element you are assigning
Fix now
Replace the covariant array with IReadOnlyList<T> for read access or List<T> for mutable access. Code: IReadOnlyList<FinancialEvent> safeBuffer = new List<FinancialEvent>(batch);
Compile error CS1961 ('Invalid variance: The type parameter T must be contravariantly valid')+
Immediate action
Remove the offending method parameter that accepts T from the covariant (`out`) interface — or accept that the interface cannot be covariant
Commands
PowerShell: dotnet build /p:TreatWarningsAsErrors=false 2>&1 | Select-String -Pattern "CS1961" | bash: dotnet build /p:TreatWarningsAsErrors=false 2>&1 | grep "CS1961"
PowerShell: Get-ChildItem -Recurse *.cs | Select-String -Pattern "out T" | bash: grep -rn "out T" --include="*.cs" .
Fix now
Remove the method that accepts T from the interface, or split into two interfaces: one covariant producer (out T) and one invariant writer.
Contravariant assignment fails when it should compile (e.g., IComparer<Animal> cannot assign to IComparer<Dog>)+
Immediate action
Verify that IComparer<T> in the version of .NET you are targeting is defined with `in T` — older .NET Framework versions may not have variance on this interface
Commands
PowerShell: dotnet --version | bash: dotnet --version — .NET Framework 4.5+ and all .NET Core/.NET 5+ versions have variance on IComparer<T>
In Immediate Window: `typeof(System.Collections.Generic.IComparer<>).GetGenericArguments()[0].GenericParameterAttributes` — look for Contravariant in the flags
Fix now
Upgrade target framework if needed. If you are on a supported framework and it still fails, define a custom contravariant interface: interface IMyComparer<in T> { int Compare(T x, T y); }
InvalidCastException when casting Func<Dog> to Func<Animal> at runtime+
Immediate action
Stop casting between constructed generic delegate instances — this is not how delegate variance works. Switch to method group assignment.
Commands
PowerShell: Get-ChildItem -Recurse *.cs | Select-String -Pattern "\(Func<" | bash: grep -rn "(Func<" --include="*.cs" . — find all explicit delegate casts and audit each one
In Immediate Window on the failing line: `? existingDelegate.Method.Name` — then reassign via method group: `Func<Animal> safe = existingDelegate.Method.CreateDelegate<Func<Animal>>(existingDelegate.Target)`
Fix now
Replace (Func<Animal>)dogFunc with Func<Animal> f = dogMethod where dogMethod is the original method group, or wrap: Func<Animal> f = () => dogFunc()
Covariance vs Contravariance Comparison
AspectCovariance (out)Contravariance (in)
Keywordoutin
Assignment directionDerived → Base (e.g. IProducer<Dog> → IProducer<Animal>)Base → Derived (e.g. IProcessor<Animal> → IProcessor<Dog>)
T allowed in return types?Yes — only in output positionsNo — CS1962 compile error
T allowed in method parameters?No — CS1961 compile errorYes — only in input positions
Real-world roleProducers, factories, read-only sequencesConsumers, comparers, handlers, validators
BCL examplesIEnumerable<out T>, IReadOnlyList<out T>, Func<out TResult>IComparer<in T>, Action<in T>, IEqualityComparer<in T>
Works on classes?No — only interfaces and delegates; CS1960 on classesNo — only interfaces and delegates; CS1960 on classes
Array support?Yes (unsound, CLR store check on every write — treat as deprecated)Arrays are covariant only — contravariance does not apply to arrays
Compile-time safe?Yes, fully verified by compilerYes, fully verified by compiler
Mutability requirementType must be read-only with respect to TType must be write-only (consume-only) with respect to T
Delegate instance castingNot safe — cast Func<Dog> to Func<Animal> throws InvalidCastException at runtimeNot safe — cast Action<Animal> to Action<Dog> throws InvalidCastException at runtime

Key takeaways

1
Variance is a compile-time-verified contract about data flow direction
'out' means T only exits the type (producers), 'in' means T only enters the type (consumers). The compiler rejects violations at CS1961 or CS1962. These errors are telling you the abstraction is doing too much — split it.
2
Covariance flows WITH inheritance
IEnumerable<Dog> is assignable to IEnumerable<Animal>. Contravariance flows AGAINST it: IComparer<Animal> is assignable to IComparer<Dog>. The mnemonic that holds: producers widen output upward, consumers widen input downward.
3
Array covariance (Dog[] to Animal[]) is a legacy design flaw
the CLR store check fires on every write through a widened reference, and any write of an incompatible type throws ArrayTypeMismatchException at runtime. Treat it as a deprecated pattern. In all new code, use IReadOnlyList<out T> for covariant views and List<T> for mutable collections.
4
Variance only applies to interfaces and delegates in C#
never to classes or structs. CS1960 is the compiler telling you to extract an interface. The implementing class stays invariant; the interface carries the variance contract.
5
Delegate variance is automatic for method group assignments. Never cast delegate instances between constructed generic types
Func<Dog> to Func<Animal> throws InvalidCastException at runtime even though the types are variance-compatible. Always assign from a method group or wrap in a lambda. Span<T> is a ref struct and cannot participate in generic variance — it is a performance tool, not a covariant abstraction.

Common mistakes to avoid

6 patterns
×

Assuming List<Dog> is assignable to List<Animal>

Symptom
CS0029 compile error: 'Cannot implicitly convert type List<Dog> to List<Animal>'. Beginners often add a cast that compiles but fails at runtime with InvalidCastException.
Fix
Use IEnumerable<Dog> or IReadOnlyList<Dog> when you only need read access — both are covariant and the assignment to their Animal counterparts is legal and safe without any cast. If you need a mutable collection that accepts both Dogs and Cats, use List<Animal> from the start.
×

Trying to declare variance on a class instead of an interface

Symptom
CS1960 compile error: 'Invalid variance modifier. The type parameter T of IProducer<T> must be invariantly valid.' Occurs when in or out is placed on a class or struct type parameter.
Fix
Extract an interface (e.g. IRepository<out T>) and apply the variance keyword there. Keep the concrete class invariant. The class implements the interface without needing any variance annotation of its own.
×

Putting an 'out' type parameter in a method parameter position

Symptom
CS1961 compile error: 'Invalid variance: The type parameter T must be contravariantly valid on this interface.' The interface has out T but a method accepts T as input.
Fix
Either remove the method that accepts T, replace the parameter type with a non-variant alternative, or acknowledge that the interface cannot be covariant and remove out. If you need both, split into two interfaces: one producer with out T and one with an invariant T for the accepting methods.
×

Putting an 'in' type parameter in a return position

Symptom
CS1962 compile error: 'Invalid variance: The type parameter T must be covariantly valid on this interface.' The interface has in T but a method returns T.
Fix
Either remove the return type that produces T, change the return type to a non-variant alternative, or remove in and make the interface invariant. If both consuming and producing T are genuinely needed, the type parameter must be invariant.
×

Using array covariance in new API signatures

Symptom
ArrayTypeMismatchException at runtime when a caller writes through a widened array reference. The CLR covariant store check fires and the exception is thrown — but only at runtime, often in production.
Fix
Replace array parameters with IReadOnlyList<T> for covariant read-only access. If mutability is needed internally, use List<T> and expose IReadOnlyList<T> publicly. Never accept or return raw arrays in new public APIs where variance is involved.
×

Casting delegate instances between constructed generic types

Symptom
InvalidCastException at runtime when attempting to cast Func<Dog> to Func<Animal> (or Action<Animal> to Action<Dog>). The compiler may not flag the cast in all contexts, but the CLR will reject it at runtime because the underlying types are different.
Fix
Use method group assignment instead of delegate instance casting. If you only have a delegate instance and not the original method, wrap it: Func<Animal> animalFunc = () => existingDogFunc(). This creates a new delegate that is correctly typed without an unsafe cast.
INTERVIEW PREP · PRACTICE MODE

Interview Questions on This Topic

Q01SENIOR
Can you explain the difference between covariance and contravariance in ...
Q02SENIOR
Why is List invariant while IEnumerable is covariant, even though ...
Q03SENIOR
Array covariance has been in C# since version 1.0. What is the problem w...
Q04SENIOR
Can you explain how delegate variance works in C#? Give an example of bo...
Q01 of 04SENIOR

Can you explain the difference between covariance and contravariance in C# generics, and give a concrete example of each from the BCL?

ANSWER
This is one of those questions where the answer lives in understanding why the feature exists, not just what it does — so let's start there. Covariance (out) allows a generic type to be used with a more derived type than originally specified. The BCL example most people have used without realising it is IEnumerable<out T>: an IEnumerable<Dog> can be assigned to IEnumerable<Animal>. The reason it's safe is that IEnumerable only ever produces T values — it never accepts T as input. There is no mechanism through which you could inject a wrong-typed object. The compiler verifies this contract; if you try to add a method that accepts T on an 'out' interface, you get CS1961 immediately. Contravariance (in) runs in the opposite direction. IComparer<in T> is the canonical example: an IComparer<Animal> can be used where an IComparer<Dog> is expected, because a comparer that handles any Animal can certainly handle a Dog. The assignment goes against the inheritance arrow, which is why it surprises people the first time. It's safe because IComparer only consumes T — it never returns it. Both modifiers are compile-time contracts, not runtime magic. The compiler fully verifies them at the point of interface declaration, so if you violate the contract, you hear about it before anything ships.
FAQ · 6 QUESTIONS

Frequently Asked Questions

01
What is the difference between covariance and contravariance in C#?
02
Why can't I assign List to List in C#?
03
Does variance work with classes in C#, or only interfaces?
04
What is the difference between array covariance and generic covariance?
05
How do I make my own interface covariant or contravariant?
06
Can I cast a Func to Func since delegate covariance exists?
🔥

That's OOP in C#. Mark it forged?

10 min read · try the examples if you haven't

Previous
Indexers in C#
10 / 10 · OOP in C#
Next
LINQ in C#