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

📚 Interview question

This code compiles but throws ArrayTypeMismatchException at run time: object[] items = new string[3]; items[0] = 42;. Explain array covariance in C#, why it exists, what it costs, and how generic variance and read-only interfaces avoid the problem.

Array covariance in .NET: why string[] converts to object[], the run-time store check and ArrayTypeMismatchException, its hidden performance cost, the Span<T> constructor trap, and safe alternatives through generic variance.

Mid-Level Debugging Advanced arrays covariance csharp generics type-safety variance

🎯 Key points

  • Arrays of reference types are covariant: a string[] can be used where object[] or Base[] is expected
  • Writes are type-checked at run time; storing an incompatible element throws ArrayTypeMismatchException
  • Covariance only applies to reference-type elements: int[] does not convert to object[]
  • Every store into an array of a non-sealed reference type needs a run-time type check, a small hidden cost
  • new Span<T>(array) throws ArrayTypeMismatchException if the array's actual element type is not exactly T
  • Generic collections like List<T> are invariant, so the compiler catches the mistake
  • IEnumerable<out T> and IReadOnlyList<out T> are safely covariant because they only produce values
  • Prefer read-only covariant interfaces in APIs instead of passing arrays as base-type arrays

⏱ 30-second 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.

🗣 Full answer (senior)

How I would answer this in an interview

The code compiles because C# supports array covariance for reference types: a string[] can be treated as an object[]. But the actual object is still a string[], so storing an int (boxed into an object) into it would break type safety. The runtime therefore checks every store into a reference-type array and throws ArrayTypeMismatchException. I'd explain why the feature exists, its runtime cost, and how generics give safe variance instead.

1. What happens step by step

object[] items = new string[3];  // allowed: string[] -> object[] (covariant reference conversion)
items[0] = "ok";                 // runtime check: is string assignable to string? yes
items[0] = 42;                   // 42 is boxed to object; runtime check: is Int32 assignable to string? NO
                                 // -> ArrayTypeMismatchException
  • At compile time, the static type is object[], so assigning any object looks legal.
  • At run time, the array object's actual element type is string. The stelem.ref IL instruction checks the stored value's type against that element type on every write.

2. Why array covariance exists

  • It came from Java compatibility goals in the original .NET design (Java has the same unsound array covariance), and it was in the CLR before generics existed (.NET 1.0 had no generics).
  • It allowed writing "generic-ish" methods like void PrintAll(object[] items) or Array.Sort(object[]) that work with any reference-type array.
  • It only applies to reference-type elements: int[] cannot be converted to object[] (elements would need boxing, changing the memory layout). int[] can be cast to uint[] at the CLR level, a quirk sometimes used in low-level code.

It is widely considered a design mistake (Eric Lippert called it "broken"): it trades compile-time safety for convenience and imposes a cost on every reference array write.

3. What it costs

  • Correctness: errors move from compile time to run time, and can be far from where the conversion happened (e.g. a method receives object[] and writes to it, unaware it is really a string[]).
  • Performance: every store to a reference-type array goes through a covariance type check (a helper call StelemRef), unless the JIT can prove the array's exact type. It is fast when the element type matches exactly (a pointer comparison), slower when it has to walk the type hierarchy or check interfaces. In hot loops writing to object[], this check is measurable.
  • Related: Span<T> creation from an array (new Span<T>(array)) checks that the array is exactly T[] and throws ArrayTypeMismatchException otherwise, because a writable span would bypass the per-store check.

4. How generic variance avoids the problem

Generic variance in C# 4 is safe because it is restricted by how the type parameter is used:

Interface Variance Why safe
IEnumerable<out T> Covariant You can only read T
IReadOnlyList<out T>, IReadOnlyCollection<out T> Covariant Read-only
IComparer<in T>, IEqualityComparer<in T>, Action<in T> Contravariant You can only pass in T
Func<out TResult> Covariant in result Output only
List<T>, IList<T>, ICollection<T> Invariant Both read and write
IEnumerable<object> objs = new List<string>();      // OK: covariance, read-only
IReadOnlyList<object> ro = new[] { "a", "b" };      // OK
List<object> bad = new List<string>();              // compile error - invariant
IList<object> alsoBad = new List<string>();         // compile error

Because writes aren't possible through a covariant interface, the compiler can allow the conversion safely, and no runtime check is needed.

5. How I write code to avoid it

  • Accept IEnumerable<T>, IReadOnlyList<T> or ReadOnlySpan<T> instead of object[]/T[] when the method only reads.
  • Use generic methods (void Process<T>(T[] items)) rather than object[] parameters.
  • Avoid object[] as a general container; if needed, create real object[] arrays (new object[n]), not covariant views of more specific arrays.
  • sealed element types let the JIT remove some covariance checks because no subtypes can exist.

6. A production example

A reporting library had void Fill(object[] row) that wrote formatted values into the array it was given. A caller passed string[] row = new string[columns]; to avoid casting later, and everything worked until a new column type wrote a decimal — the job crashed with ArrayTypeMismatchException deep inside the library. The fix was changing the API to void Fill(Span<object?> row) (which throws immediately at the call site if given a string[]) and later to returning a typed record per row. The lesson: covariant arrays hide a contract violation until data changes.

🔍 What the interviewer is checking

A debugging question that probes type-system depth.

Green flags

  • Explains covariance and the run-time store check.
  • Knows value-type arrays are not covariant and generics are invariant unless out/in.
  • Mentions the performance cost and the Span<T> constructor check.
  • Recommends read-only covariant interfaces.

Red flags

  • Cannot explain why the code compiles.
  • Confuses covariance with inheritance in general.
  • Suggests catching the exception as the fix.

📚 Learn in depth

1. The IL and the runtime check

Storing into a reference-type array uses stelem.ref (or stelem with a reference type token). The JIT expands it into a call to a helper (CORINFO_HELP_ARRADDR_ST / StelemRef), which roughly does:

if (value == null) store; return;
elementType = array.MethodTable.ElementType;
if (value.MethodTable == elementType) store; return;     // fast path: exact match
if (elementType == typeof(object)) store; return;         // object[] accepts anything
if (IsAssignable(value.MethodTable, elementType)) store; // slow path: walk hierarchy / interfaces / cast cache
else throw ArrayTypeMismatchException;

The helper also performs the GC write barrier that every reference store needs. For value-type arrays (int[]), there is no covariance check and no write barrier.

2. When the JIT removes the check

  • If the array's static type is T[] where T is sealed (e.g. string[]), the actual array can only be string[], so any string store is safe; the JIT can skip the type check.
  • If the array was just created with new T[n] in the same method, its exact type is known.
  • If the value being stored is null.
  • If the value comes from the same array (e.g. swapping elements in a sort).

That's why sorting or shuffling a string[] is cheap, but writing arbitrary objects to an object[] that might be a covariant view is not.

3. Covariance and Span<T> / Memory<T>

object[] arr = new string[3];
var span = new Span<object>(arr);   // throws ArrayTypeMismatchException
ReadOnlySpan<object> ro = arr;      // OK - read-only, no writes possible

Writable spans write elements with plain memory stores (no per-element check), so the constructor validates up front that arr.GetType() == typeof(object[]). This means some code paths throw just from creating a span over a covariant array, even if you never write.

4. Value-type array oddities

The CLR allows reinterpretation between same-size integral arrays of different signedness:

object o = new int[] { -1 };
uint[] u = (uint[])o;          // allowed by the CLR! u[0] == 4294967295

C# rejects (uint[])new int[1] at compile time, but casting through object succeeds at run time. Enum arrays and their underlying integer arrays are similarly interchangeable. These are rarely used intentionally but can surprise in reflection-heavy code.

5. Variance rules for generics (how C# keeps them safe)

  • Variance annotations (out, in) are only allowed on interface and delegate type parameters, not classes or structs.
  • An out T parameter may appear only in output positions (return types, get accessors, out T in other covariant interfaces).
  • An in T parameter may appear only in input positions.
  • Variance applies only to reference type arguments: IEnumerable<int> is not convertible to IEnumerable<object>, because that would require boxing each element.
  • The compiler verifies these rules at the declaration (CS1961), so uses can never break type safety.

The classic example of contravariance: an IComparer<object> can be used where an IComparer<string> is expected, because anything that compares objects can compare strings.

6. Arrays and generic interfaces

SZ arrays implement IList<T>, IReadOnlyList<T> and friends through runtime magic (SZArrayHelper). Because of array covariance, a string[] also "implements" IList<object>, and writing through that interface triggers the same runtime check. Calling Add on an array via IList<T> throws NotSupportedException, because arrays are fixed-size. These are more reasons not to expose arrays as IList<T>.

Try it yourself

  1. Run the failing snippet and read the stack trace.
  2. Benchmark writing object values into a real object[] vs into object[] arr = new string[n] (storing strings) with BenchmarkDotNet.
  3. Try new Span<object>(new string[1]) and ReadOnlySpan<object> r = new string[1];.
  4. Declare an interface IProducer<out T> { void Accept(T value); } and read CS1961.

💻 Code

namespace Arrays;

public abstract class Animal { public abstract string Sound { get; } }
public sealed class Dog : Animal { public override string Sound => "Woof"; }
public sealed class Cat : Animal { public override string Sound => "Meow"; }

public static class Covariance
{
    public static void Main()
    {
        // 1. Covariant array, checked writes
        object[] items = new string[3];
        items[0] = "ok";
        try
        {
            items[1] = 42;
        }
        catch (ArrayTypeMismatchException)
        {
            Console.WriteLine($"Array is really {items.GetType().Name}");   // String[]
        }

        // 2. Base-type array that is really a Dog[]
        Animal[] animals = new Dog[2];
        animals[0] = new Dog();
        try { animals[1] = new Cat(); }
        catch (ArrayTypeMismatchException) { Console.WriteLine("Cannot store a Cat in a Dog[]"); }

        // 3. Span constructor also checks
        try { _ = new Span<Animal>(animals); }
        catch (ArrayTypeMismatchException) { Console.WriteLine("Writable span refused"); }
        ReadOnlySpan<Animal> readOnly = animals;            // fine: cannot write

        // 4. Safe alternative: generic covariance on read-only interfaces
        List<Dog> dogs = [new Dog(), new Dog()];
        IReadOnlyList<Animal> view = dogs;                  // allowed, cannot add a Cat
        PrintAll(view);
        // List<Animal> wrong = dogs;                       // compile error: List<T> is invariant
    }

    private static void PrintAll(IEnumerable<Animal> animals)
    {
        foreach (var a in animals) Console.WriteLine(a.Sound);
    }
}

⚠ Common mistakes

1. Writing to arrays received as object[]

What happens: a method takes object[] and writes results into it. Why it is wrong: callers can pass a covariant string[] (or Customer[]), and writes fail at run time with ArrayTypeMismatchException. Do instead: return new data, use generic parameters (T[]), or accept Span<object?> which fails fast at the call site.

2. Thinking array covariance is type-safe because it compiles

What happens: object[] items = new Order[10]; treated as a general-purpose object array. Why it is wrong: the compiler allows it, but the runtime enforces the real element type on every write. Do instead: create the array with the type you intend to store, or use read-only interfaces for covariance.

3. Expecting List<Derived> to convert to List<Base>

What happens: surprise at a compile error for List<object> l = new List<string>();. Why it is wrong: List<T> is invariant because it supports both reads and writes — allowing it would recreate the array problem. Do instead: use IEnumerable<Base> or IReadOnlyList<Base> for read-only covariance, or create a new list with Cast<Base>().ToList().

4. Expecting variance with value types

What happens: passing IEnumerable<int> where IEnumerable<object> is expected. Why it is wrong: variance only applies to reference types; value types would need boxing. Do instead: use .Cast<object>() (boxes each element) or make the consuming method generic.

5. Using object[] as a general container in hot paths

What happens: object[] arrays filled in tight loops (e.g. parameter arrays, row buffers). Why it is wrong: each store has a covariance check and write barrier; value types are boxed. Do instead: use typed arrays, structs, or generic buffers.

6. Creating writable spans over possibly covariant arrays

What happens: new Span<Base>(arrayOfBase) where the array may actually be Derived[]. Why it is wrong: the constructor throws ArrayTypeMismatchException. Do instead: use ReadOnlySpan<Base> for reading, or ensure the array's exact type.

7. Casting through object to reinterpret arrays

What happens: (uint[])(object)intArray used as a trick. Why it is wrong: confusing, relies on CLR quirks, and breaks with tools that assume C# rules. Do instead: use MemoryMarshal.Cast<int, uint>(span) explicitly when reinterpretation is intended.

8. Exposing arrays through IList<T>

What happens: returning a string[] typed as IList<string> or IList<object>. Why it is wrong: callers may call Add (throws NotSupportedException) or write through covariance (throws ArrayTypeMismatchException). Do instead: return IReadOnlyList<T> or ImmutableArray<T>.

9. Not sealing element types used in hot arrays

What happens: arrays of non-sealed classes written frequently. Why it is wrong: the JIT can't skip covariance checks because subclasses might exist. Do instead: seal classes that aren't designed for inheritance (CA1852) — it also helps devirtualisation.

10. Misexplaining the exception

What happens: candidates blame boxing or a cast error rather than covariance. Why it is wrong: misses the real mechanism — the runtime element-type check on store. Do instead: explain static vs runtime array type and the stelem.ref check.

🏛 Senior insight: trade-offs

Array covariance is a legacy design flaw; seniors avoid relying on it and design APIs with generic variance instead.

API need Signature
Read a sequence of base types IEnumerable<Animal> / IReadOnlyList<Animal>
Compare or consume base types IComparer<in T>, Action<in T>
Mutate a collection exact List<T> / T[]
High-performance read ReadOnlySpan<T>
  • Mentioning that out and in variance is checked at compile time while array covariance is checked at run time shows real type-system understanding.
  • The performance cost is small but real in tight loops; the BCL avoids it with sealed types and struct wrappers in hot code.
  • Avoid public APIs that take object[] or base-class arrays; they invite this bug.

🔗 Follow-ups the interviewer may ask

  1. When: Candidate is unsure how arrays store references Why: Grounds covariance in array basics
    How to answer

    An array is a fixed-length, zero-based, contiguous block of elements of one type. T[] is a reference type, and new int[5] gives five zeros. I create them with new T[n], initialisers or collection expressions like int[] ids = [1, 2, 3]. The length never changes: Array.Resize actually allocates a new array and copies. Clone is a shallow copy, arr[1..3] copies a range while arr.AsSpan(1..3) is a free view, and Array.Sort/BinarySearch are the built-in algorithms. I use arrays for fixed-size data, buffers and hot loops, and List<T> when the number of items changes.

  2. When: Candidate notes covariance does not apply to int[] Why: Checks value versus reference type understanding
    How to answer

    A value type holds its data directly, so assigning or passing it copies the data; a reference type variable holds a reference to an object on the managed heap, so copying the variable makes two references to the same object. Value types are stored inline where they are declared: a local usually sits on the stack or in a register, but a struct field of a class lives inside that object on the heap, and captured or boxed values also end up on the heap. By default classes compare by identity and structs by their fields. I use structs for small, immutable values like Money or coordinates, and classes for everything with identity or behaviour.

🛠 Real-world problems: how you handle them

Start-up crash from writing into a covariant handler array
Situation
A payments platform loaded plug-in handlers at start-up into an IHandler[] variable that actually referenced a PaymentHandler[] built by one module; another module appended a logging handler into a spare slot.
Signals
In one tenant configuration the service crashed on start-up with ArrayTypeMismatchException; the stack trace pointed at the handler registration code.
Investigation
Captured the exception in Application Insights, printed items.GetType() in a debug build to see PaymentHandler[], and traced where the array was created and shared.
Resolution
Replaced the shared array with a List<IHandler> built by concatenating module handlers, and changed the module API to return IReadOnlyList<IHandler>.
Prevention
A code review guideline against base-type arrays in APIs, and an integration test that starts the host with every tenant configuration.

How to tell it (STAR)

**S:** Our payments service [at company] crashed at start-up for one tenant configuration. **T:** I had to find out why only that configuration failed. **A:** I traced the ArrayTypeMismatchException to a handler array that was really a derived-type array because of covariance, replaced it with a list of the interface type, and changed the module API to read-only interfaces. **R:** The crash was fixed the same day and the configuration test now covers every tenant.

↩ Usually asked after

More in Arrays

Last updated 30 Oct 2026.

🌐 Sangam's Webring

⏪ Previous 🏠 Main Next ⏩
💬 Chat with me