Generics

C# · Practical · 12 min

Generics power all collections and most libraries, and let you avoid casts and boxing.

Generics let a class or method work with any type while staying type-safe: List<T>, Dictionary<TKey, TValue>, Task<T>. You write the code once with a type parameter and the compiler checks every use.

Generic method: static T Max<T>(T a, T b) where T : IComparable<T> => a.CompareTo(b) >= 0 ? a : b;. The type argument is usually inferred: Max(3, 7).

Generic class: class Box<T> { public T Value { get; set; } }, used as new Box<string>().

Constraints (where clauses) say what T must support, so you can call those members:

  • where T : class / where T : struct (reference or value type), where T : notnull.
  • where T : new() requires a parameterless constructor.
  • where T : SomeBase or where T : IComparable<T> for a base class or interface.
  • where T : INumber<T> (generic math, .NET 7+) lets you use + and T.Zero on any numeric type.

Unlike Java, .NET generics are reified: the runtime knows List<int> from List<string>, typeof(T) works, and value-type instantiations get specialized code without boxing.

default(T) (or just default) gives null for reference types and zero for value types. Prefer generics over object parameters: you keep compile-time checks and avoid casts.

Example

using System.Numerics;

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

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

Console.WriteLine(Max(3, 7));             // 7
Console.WriteLine(Max("pear", "apple"));  // pear
Console.WriteLine(Sum([1.5, 2.5]));       // 4
Console.WriteLine(Sum([1, 2, 3]));        // 6

var box = new Box<int>(42);
Console.WriteLine(box);                   // Box<Int32>(42)

class Box<T>(T value)
{
    public T Value { get; } = value;
    public override string ToString() => $"Box<{typeof(T).Name}>({Value})";
}

Remember

  • Type parameters give reuse without casts or boxing
  • Constraints (where T : ...) unlock members you can call on T
  • Type arguments are usually inferred for generic methods
  • .NET generics are reified: typeof(T) works at runtime

Check yourself

  1. Why does Max<T>(T a, T b) need where T : IComparable<T> to call a.CompareTo(b)?

Sign in to answer, see the explanations, and get this lesson back as a quick review later.

Official docs: learn.microsoft.com/en-us/dotnet/csharp/fundamentals/types/generics