Generics let you write code that works with any type while keeping full type safety. Instead of writing separate classes or methods for int, string, and every other type you need, write one version with one or more type parameters (such as T, or TKey and TValue) and specify the actual types when you use it.
A generic method declares its own type parameter. The compiler often infers the type argument from the values you pass, so you don't need to specify it explicitly.
static void Print(T value) => Console.WriteLine($"Value: {value}");
Print(42); // Compiler infers T as int
Print("hello"); // Compiler infers T as string
Print(3.14); // Compiler infers T as double
Use square brackets (from C# 12) instead of constructor calls or initializer syntax.
The spread operator (..) inlines the elements of one collection into another, which is useful for combining sequences
You can initialise dictionaries concisely with indexer initializers. This syntax uses square brackets to set key-value pairs.
Constraints restrict which type arguments a generic type or method accepts. Constraints let you call methods or access properties on the type parameter that wouldn't be available on object alone.
| Constraint | Meaning |
|---|---|
where T : class |
T must be a reference type |
where T : struct |
T must be a non-nullable value type |
where T : new() |
T must have a public parameterless constructor |
where T : BaseClass |
T must derive from BaseClass |
where T : IInterface |
T must implement IInterface |
Covariance and contravariance describe how generic types behave with inheritance. They determine whether you can use a more derived or less derived type argument than originally specified.