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:
Tfor 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:
| Constraint | Meaning |
|---|---|
where T : ClassName | T must be ClassName or a derived class |
where T : InterfaceName | T must implement InterfaceName |
where T : class | T must be a reference type |
where T : class? | T must be a reference type, possibly nullable (C# 8) |
where T : struct | T must be a non-nullable value type |
where T : new() | T must have a public parameterless constructor |
where T : notnull | T must be a non-nullable type (C# 8) |
where T : unmanaged | T must be an unmanaged type (no managed references) |
where T : U | T 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:
class,struct,notnull, orunmanaged(at most one).- Specific class.
- Interfaces.
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 aList<string>at runtime viais-test or pattern. default(T)is0for value types andnullfor 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).