Polyglot
Languages C# generics
C# § generics

Generics

C# generics are runtime-reified: the runtime knows the actual type arguments, and a List<int> is a different runtime type from List<string>. The mechanism is more capable than Java’s erased generics — reflection sees the type arguments, the JIT can specialise the implementation per type, value-type arguments avoid boxing — and admits constraints that restrict which types may instantiate a generic. The standard library uses generics throughout: collections, LINQ, async, and the type-traits-style facilities are all generic. Modern C# code uses generics extensively; the design conventions for them — when to constrain, what to constrain on, when to use generic methods versus generic types — are part of the language’s idiom.

This page covers generic types, generic methods, type constraints, variance, and the conventions for using each. The relationship to LINQ is in LINQ; the underlying delegate types are in Methods and delegates.

Generic types

A generic type takes one or more type parameters in its declaration:

public class Box<T> {
    public T Value { get; init; }
    public Box(T value) { Value = value; }
}

Box<int>     bi = new Box<int>(42);
Box<string>  bs = new Box<string>("hello");

The type parameter T stands in for an actual type; each instantiation (Box<int>, Box<string>) is a separate runtime type. The runtime allocates separate memory layout per type instantiation, specialises method bodies, and exposes the actual type through reflection.

Multiple type parameters use a comma-separated list:

public class Pair<TFirst, TSecond> {
    public TFirst  First  { get; init; }
    public TSecond Second { get; init; }
}

Pair<string, int> p = new Pair<string, int> { First = "alice", Second = 30 };

The conventional naming:

  • T for a single type parameter.
  • T1, T2, … for multiple parameters with no semantic distinction.
  • TKey, TValue, TInput, TOutput, TResult, etc., when the role suggests a name.

Generic interfaces, structs, and records work the same way:

public interface IEnumerable<out T> { /* ... */ }
public struct ValueTuple<T1, T2> { /* ... */ }
public record class Result<T>(bool Ok, T? Value, string? Error);

Generic methods

A method may have its own type parameters:

public T First<T>(IEnumerable<T> source) {
    foreach (var item in source) return item;
    throw new InvalidOperationException();
}

int    n = First(new[] { 1, 2, 3 });          // T inferred as int
string s = First(new List<string> { "a" });    // T inferred as string

Type inference operates on the argument types; the explicit form First<int>(...) is rare. The compiler infers T from the parameter types, the return type, and (since C# 7.3) constraints.

A generic method may appear inside a non-generic class or inside a generic class:

public static class Algorithms {
    public static T Max<T>(T a, T b) where T : IComparable<T>
        => a.CompareTo(b) > 0 ? a : b;
}

int  n = Algorithms.Max(3, 5);

Type constraints

The where clause restricts which types may instantiate the generic:

public class Sorter<T> where T : IComparable<T> {
    public void Sort(T[] array) { /* ... */ }
}

public T Min<T>(T a, T b) where T : IComparable<T>
    => a.CompareTo(b) < 0 ? a : b;

The constraint enables the generic body to use members of the constrained interface or type. Without the constraint, a.CompareTo(b) would not compile (the compiler does not know T has a CompareTo method).

The standard constraint forms:

ConstraintMeaning
where T : ClassNameT must be ClassName or a derived class
where T : InterfaceNameT must implement InterfaceName
where T : classT must be a reference type
where T : class?T must be a reference type, possibly nullable (C# 8)
where T : structT must be a non-nullable value type
where T : new()T must have a public parameterless constructor
where T : notnullT must be a non-nullable type (C# 8)
where T : unmanagedT must be an unmanaged type (no managed references)
where T : UT must be the same as or derived from U (where U is another type parameter)

Multiple constraints on a single parameter are comma-separated:

public class Cache<T> where T : class, IDisposable, new() {
    private T? instance;
    public T Get() => instance ??= new T();
    public void Reset() { instance?.Dispose(); instance = null; }
}

Constraints on multiple parameters are listed separately:

public class Map<TKey, TValue> where TKey : notnull where TValue : class { /* ... */ }

Constraint ordering

When multiple constraints apply, they must be listed in a specific order:

  1. class, struct, notnull, or unmanaged (at most one).
  2. Specific class.
  3. Interfaces.
  4. new() (last).
public class C<T> where T : SomeBaseClass, IDisposable, new() { /* ... */ }

The compiler enforces the order; the syntax catches misorderings as compile-time errors.

Variance: in and out

By default, generic types are invariant: IEnumerable<Cat> is not assignable to IEnumerable<Animal> even when Cat : Animal. The reason is type safety: if the assignment were allowed, code that wrote a Dog to the IEnumerable<Animal> (which it could not, because IEnumerable is read-only — but the principle holds for IList<T>) would corrupt the underlying IEnumerable<Cat>.

Variance annotations declare a type parameter as covariant (out) or contravariant (in):

public interface IEnumerable<out T> {       // covariant: T appears only in output positions
    IEnumerator<T> GetEnumerator();
}

public interface IComparer<in T> {           // contravariant: T appears only in input positions
    int Compare(T x, T y);
}

out (covariance) admits IEnumerable<Cat>IEnumerable<Animal> (a producer of Cats is a producer of Animals).

in (contravariance) admits IComparer<Animal>IComparer<Cat> (a consumer of Animals can compare Cats).

IEnumerable<Cat>  cats   = GetCats();
IEnumerable<Animal> animals = cats;       // covariance

IComparer<Animal> animalCmp = new AnimalNameComparer();
IComparer<Cat>    catCmp    = animalCmp;  // contravariance

Variance applies only to interfaces and to delegate types. Class generic parameters are always invariant; structs are always invariant.

The standard library uses variance extensively:

  • IEnumerable<out T>, IEnumerator<out T>, IReadOnlyCollection<out T>, IReadOnlyList<out T>, IReadOnlyDictionary<TKey, out TValue> — covariance for read-only views.
  • Func<in T1, in T2, out TResult> — contravariance for parameters, covariance for return.
  • Action<in T> — contravariance for parameters.
  • IComparer<in T>, IEqualityComparer<in T> — contravariance.

Reified generics vs erased generics

The C# generics implementation is reified — the runtime knows the actual type arguments at run time. This contrasts with Java’s erasure, which removes type-argument information at runtime.

The consequences for C#:

  • Reflection sees the type arguments: typeof(List<int>) != typeof(List<string>).
  • The JIT specialises the implementation per type; value-type instantiations (List<int>) avoid boxing.
  • A method may take a List<int> and reject a List<string> at runtime via is-test or pattern.
  • default(T) is 0 for value types and null for reference types, determined at runtime.
  • typeof(T) is admissible inside a generic method/type and yields the actual type.

The principal practical advantage is performance: List<int> stores ints inline, with no boxing per element; the per-element cost is the same as a hand-written IntList.

List<int>    ints    = new List<int>();
List<string> strings = new List<string>();

bool same = ints.GetType() == strings.GetType();    // false: different types
Type t = typeof(List<int>);                          // the type itself

Generic constraints in interfaces

Interfaces may declare generic methods:

public interface IConverter {
    T Convert<T>(string input) where T : IParsable<T>;
}

Implementations supply the concrete method:

public class Converter : IConverter {
    public T Convert<T>(string input) where T : IParsable<T>
        => T.Parse(input, null);
}

The constraint must match between the interface declaration and the implementation; the compiler enforces.

Generic methods on non-generic types

A non-generic class may have generic methods. The standard library has many:

public static class Enumerable {
    public static IEnumerable<TResult> Select<TSource, TResult>(
        this IEnumerable<TSource> source,
        Func<TSource, TResult> selector
    ) { /* ... */ }

    public static T First<T>(this IEnumerable<T> source) { /* ... */ }
}

The conventional pattern: a static utility class with generic methods, often as extension methods. LINQ’s standard query operators are built this way.

Generic delegates

The standard library provides generic delegate types:

Func<int, int>      square  = x => x * x;
Action<string>      log     = s => Console.WriteLine(s);
Predicate<int>      isEven  = n => n % 2 == 0;
Comparison<string>  cmpLen  = (a, b) => a.Length - b.Length;

Custom generic delegate types are rare; the Func<> and Action<> families with up to 16 parameters cover almost every use.

Common patterns

A generic container

public class Stack<T> {
    private T[] items = new T[16];
    private int count = 0;

    public void Push(T item) {
        if (count == items.Length) Array.Resize(ref items, items.Length * 2);
        items[count++] = item;
    }

    public T Pop() {
        if (count == 0) throw new InvalidOperationException();
        return items[--count];
    }

    public T Peek() {
        if (count == 0) throw new InvalidOperationException();
        return items[count - 1];
    }

    public int Count => count;
}

The pattern is the conventional generic-container shape; the standard library’s Stack<T> provides essentially the same.

A generic algorithm with constraints

public static T Max<T>(IEnumerable<T> source) where T : IComparable<T> {
    using var it = source.GetEnumerator();
    if (!it.MoveNext()) throw new InvalidOperationException("source is empty");

    T best = it.Current;
    while (it.MoveNext()) {
        if (it.Current.CompareTo(best) > 0) best = it.Current;
    }
    return best;
}

The constraint where T : IComparable<T> admits CompareTo. The standard library has Enumerable.Max, which is essentially this.

A generic factory

public class Pool<T> where T : new() {
    private readonly Stack<T> available = new Stack<T>();

    public T Get() => available.Count > 0 ? available.Pop() : new T();
    public void Return(T item) => available.Push(item);
}

The new() constraint admits new T() in the body. The pattern is conventional for object pools.

A generic dictionary

public interface IConverter<TFrom, TTo> {
    TTo Convert(TFrom value);
}

public class Pipeline<TIn, TOut> {
    private readonly List<object> stages = new();

    public Pipeline<TIn, TNext> Then<TNext>(IConverter<TOut, TNext> stage) {
        stages.Add(stage);
        return new Pipeline<TIn, TNext> { stages = this.stages };
    }
    // ...
}

The construction admits a fluent pipeline API where each stage’s input type matches the previous stage’s output type.

Static abstract members in interfaces (C# 11)

C# 11 introduced static abstract members on interfaces — the foundation for generic math and similar interfaces:

public interface IAdditive<T> where T : IAdditive<T> {
    static abstract T Zero { get; }
    static abstract T operator +(T a, T b);
}

public struct Money : IAdditive<Money> {
    public long Cents { get; init; }
    public static Money Zero => new Money { Cents = 0 };
    public static Money operator +(Money a, Money b) =>
        new Money { Cents = a.Cents + b.Cents };
}

public static T Sum<T>(IEnumerable<T> source) where T : IAdditive<T> {
    T total = T.Zero;
    foreach (var x in source) total = total + x;
    return total;
}

The construction admits generic functions over types that share a static interface — the C# answer to languages with type classes (Haskell) or numeric protocols (Swift). The .NET 7 standard library uses static abstract members extensively in System.Numerics.

A note on type erasure

C# generics are reified, but the type parameter is not — T is not itself a runtime concept; it is a placeholder that, after instantiation, is replaced by an actual type.

The principal practical consequence: typeof(T) works at runtime (because the runtime knows the actual type), but where T : T2 is not a runtime check — the constraint is verified at compile time, not enforced at runtime through type-tag inspection.

The mechanism is what makes C# generics fast (the JIT specialises the implementation) and what makes them safe (the compiler verifies the constraints at compile time).