C# and .NET path · Types and memory · Topic 6 of 14

Generics: reified, not erased

Not started

Generics in .NET are part of IL and the runtime type system, not just the compiler. When a program uses List<int>, the runtime creates a real type for that instantiation, with its own method table and its own copy of static fields. This is called reification, and it is the main difference from Java, where the compiler checks generic types and then erases them to Object.

Why it matters: Reified generics are why List<int> doesn't box, why typeof(T) and new T() work, and how .NET differs from Java's type erasure.

  1. 1Types exist at run time

    List<int> and List<string> are different types to the runtime, with their own type objects and static fields.

  2. 2Shared or specialised code

    The JIT compiles one shared copy of a generic method for all reference types, and a separate, specialised copy for each value type.

  3. 3T is usable

    You can write typeof(T), default(T), o is List<T> and, with a new() constraint, new T().

  4. 4Constraints add capabilities

    where T : struct, class, notnull, new(), a base class, or an interface (including static abstract members, such as INumber<T>).

1

How it works

The explanation, with examples you can run.

The runtime sees T

Console.WriteLine(typeof(List<int>) == typeof(List<string>));Console.WriteLine(typeof(List<int>));Console.WriteLine(Make<Widget>().Name);Console.WriteLine(IsList<int>(new List<int>()));Console.WriteLine(IsList<int>(new List<long>())); static T Make<T>() where T : new() => new T();static bool IsList<T>(object o) => o is List<T>;class Widget { public string Name => "widget"; }

On .NET 10 this printed False, System.Collections.Generic.List`1[System.Int32], widget, True, False. Each line is something Java's erasure rules out: distinct runtime types, new T(), and a type test against a parameterised type.

How the JIT compiles generic code

Reifying every instantiation separately would multiply code size, so the runtime splits the cases:

  • Reference type arguments share code. Every reference is a pointer of the same size, so List<string>, List<Customer> and List<object> run the same machine code. That shared code finds the actual type, when it needs it, through a hidden generic context.
  • Value type arguments get their own code. List<int>, List<double> and List<MyStruct> each get code compiled for that size and layout. Elements are stored unboxed in a real int[] or MyStruct[], and calls such as EqualityComparer<T>.Default.Equals can be inlined.

This is why the generic collections replaced ArrayList and Hashtable: they keep value types unboxed and type checks happen at compile time.

Constraints

A type parameter with no constraint can be used only as object. Constraints unlock more:

static T Sum<T>(IEnumerable<T> items) where T : INumber<T>{    T total = T.Zero;    foreach (var x in items) total += x;     // operator + from the interface    return total;}

INumber<T> uses static abstract interface members (C# 11), so one method works for int, decimal or double, and because each value type gets its own compiled copy, the addition is a single machine instruction. Other useful constraints are struct, class, notnull, unmanaged, new(), a base class and allows ref struct (C# 13, for spans).

Variance

Generic interfaces and delegates can be covariant (out T) or contravariant (in T): an IEnumerable<string> can be used as an IEnumerable<object>, and an Action<object> as an Action<string>. Variance applies only to interfaces and delegates, only for reference types, so List<string> is never a List<object> and IEnumerable<int> is not an IEnumerable<object>.

2

Check yourself

3 questions. Get them all right to finish this topic.

  1. 1.What does this print in C#?
    Console.WriteLine(typeof(List<int>) == typeof(List<string>));
  2. 2.Why does List<int> store its numbers without boxing?
  3. 3.Which of these is allowed in a generic method `static T Make<T>() where T : new()`?

0 of 3 right so far. Get all three to finish this topic.