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>andList<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>andList<MyStruct>each get code compiled for that size and layout. Elements are stored unboxed in a realint[]orMyStruct[], and calls such asEqualityComparer<T>.Default.Equalscan 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>.
Check yourself
3 questions. Get them all right to finish this topic.
0 of 3 right so far. Get all three to finish this topic.