Generics In Depth

Course 2 · Ch 1
Generics In Depth
Reified generics — the exact opposite design choice from java2-1's own type erasure

Course 1 closed on a preview of LINQ. Course 2 opens on the mechanism underneath nearly everything else in C#'s type system — and the place where C# and Java's shared architecture diverges most sharply of all.

Generic Classes & Methods — Recap

class Box<T> { public T Value { get; set; } } static T First<T>(List<T> list) => list[0];

Same surface syntax as java2-1's own generics — a type parameter T, filled in at each use site.

Reified Generics — Real Runtime Type Preservation

List<string> strings = new(); List<int> ints = new(); strings.GetType() == ints.GetType(); // false — genuinely DIFFERENT runtime types

Here's the central reversal: java2-1 established that Java erases generic type information after compile-time checking, leaving List<String> and List<Integer> as the exact same runtime class. C# does the opposite — the CLR keeps real, distinct runtime type information for each type argument. List<string> and List<int> are genuinely different types at runtime, not just at compile time.

What Reification Actually Buys You

static T CreateDefault<T>() where T : new() { return new T(); // legal in C# — impossible in java2-1's Java } Console.WriteLine(typeof(T)); // also legal — typeof(T) inside a generic method

This resolves java2-1's own "consequences of erasure" list one by one: new T() works (given a constraint, covered next), typeof(T) works directly, and generic arrays of T can be created without the reflection workarounds Java's own erased generics require.

Generic Constraints

class Repo<T> where T : class {} // T must be a reference type class Box<T> where T : struct {} // T must be a value type class Sorter<T> where T : IComparable<T> {} // bounded, like java2-1's own bound

Similar spirit to java2-1's own bounded type parameters — but with more constraint kinds available. The class and struct constraints have no Java equivalent at all, precisely because Java has no reference/value type split to constrain against in the first place, unlike csharp1-2's own struct/class distinction.

Performance Implications of Reification

Here's the real payoff: List<int> in C# stores real int values directly, with zero boxing overhead — genuinely comparable to cpp2-1's own C++ templates, not to java1-8's forced-boxing List<Integer>. This directly resolves the exact gap java1-8 named as a genuine cost of Java's erased generics — a real, measurable memory and performance difference for value-heavy collections.

The Real Cost — Code Bloat, Selectively

Reification isn't free, and it isn't identical to C++'s own approach either — worth stating honestly rather than glossing over. For value types, the CLR genuinely specializes a distinct compiled implementation per type, the same class of cost cpp2-1's templates pay for every instantiation. But for reference types, the CLR is smarter: since every reference is the same pointer size underneath, the JIT can share one compiled implementation across all reference-type instantiations — List<Cat> and List<Dog> genuinely share compiled code, unlike List<int> and List<double>. This is a real, distinct middle ground between C++'s full monomorphization and Java's full erasure, not simply "closer to one or the other."

AspectC++ templates (cpp2-1)Java (java2-1)C#
Strategyfull monomorphizationtype erasurereified — selective specialization
Value-type instantiationsseparate compiled code, alwaysn/a — everything boxesseparate compiled code, like C++
Reference-type instantiationsseparate compiled code, alwaysone shared implementationone shared implementation, like Java
Runtime type info for Tn/a — resolved at compile timenone — erasedreal — typeof(T) works
The new() constraint enables real generic factories
where T : new() lets a generic method construct T directly — exactly the capability java2-1's own Challenge 2 demonstrated as flatly impossible in Java's erased generics.
Reification doesn't mean every instantiation gets separate code
Don't assume List<Cat> and List<Dog> produce two fully separate JIT'd implementations the way List<int> and List<double> do — reference-type generic instantiations genuinely share compiled code under the hood. Reification's real benefit for reference types is preserved runtime type information, not full C++-style code duplication.

Coding Challenges

Challenge 1

Create a List<string> and a List<int>, print whether their GetType() values are equal, and explain the result in a comment in terms of reification — contrasting it directly with java2-1's own erasure example.

📄 View solution
Challenge 2

Write a generic method CreateDefault<T>() constrained with where T : new() that returns new T(), and call it successfully with two different types that each have a public parameterless constructor.

📄 View solution
Challenge 3

Write a short paragraph, as a comment, explaining why List<Cat> and List<Dog> can share one compiled implementation under the hood while List<int> and List<double> cannot, tying your answer to the pointer-size argument from this chapter.

📄 View solution

Chapter 1 Quick Reference

  • C# generics are reified — real runtime type info preserved, the exact opposite of java2-1's own Java type erasure
  • new T() and typeof(T) both work directly in C# — impossible under Java's erasure without workarounds
  • Generic constraints include class/struct (no Java equivalent) alongside bounded constraints like java2-1's own
  • Value-type generic instantiations get separate compiled code (like C++); reference-type instantiations share one implementation (like Java) — a genuine middle ground, not simply "closer to one side"
  • List<int> stores real ints with zero boxing, resolving java1-8's own forced-boxing gap
  • Next chapter: LINQ in depth — deferred execution, query syntax vs. method syntax, and IEnumerable<T> vs. IQueryable<T>