Generics
Generics let you write a class, interface, method or delegate once and use it with many types, without losing type safety or paying for boxing.
Concept
Generics let you write a class, interface, method or delegate once and use it with many types, without losing type safety or paying for boxing.
class Repository<T> { public T? Get(int id)... } static T Max<T>(T a, T b) where T: IComparable<T>
IComparable<T>, IEnumerable<T>, IRepository<T> Func<T, TResult>, Action<T>, Predicate<T>
The placeholder T; by convention T, TKey, TValue, TResult Constraints (where) where T: class / class? where T: struct Non-nullable value type where T: notnull where T: new() Has a public parameterless constructor where T: BaseClass Derives from BaseClass where T: IInterface Implements the interface where T: INumber<T>
Generic math: supports +, T.Zero, … (.NET 7+) where T: unmanaged
Unmanaged value type (for pointers/spans) where T: allows ref struct May be a ref struct (C# 13)
Covariance (out T): a producer of string can be used as a producer of object — IEnumerable<string> → IEnumerable<object>.
Contravariance (in T): a consumer of object can be used as a consumer of string — Action<object> → Action<string>.
Only interfaces and delegates can be variant, and only for reference types.
Type Inference, Open and Closed Types The compiler infers type arguments from method arguments (Max(3, 9) → Max<int>). Repository<> is an open generic type definition; Repository<Vehicle> is a closed constructed type. The runtime creates separate code for each value-type argument and shares code for reference-type arguments.
Why It Matters
Every collection, LINQ, Task<T>, dependency injection and most libraries are built on generics. Writing your own generic types removes duplicated code (one repository instead of one per entity) while keeping compile-time checking.
Syntax
class Name<T> where T : constraint { }
static TResult Method<T, TResult>(T input) where T : IFace { }
interface IProducer<out T> { T Produce(); }
interface IConsumer<in T> { void Consume(T item); }Example
using System.Numerics;
// Generic class
var repo = new Repository<Vehicle>();
repo.Add(new Vehicle(1, "Aurora"));
repo.Add(new Vehicle(2, "Hauler"));
Console.WriteLine($"Get(2) = {repo.Get(2)}, Get(9) = {repo.Get(9)?.ToString() ?? "null"}, Count = {repo.Count}");
// Generic methods with type inference
Console.WriteLine($"Max(3, 9) = {Max(3, 9)}, Max(\"abc\", \"abd\") = {Max("abc", "abd")}");
Swap(ref repo, ref repo); // T inferred as Repository<Vehicle>
int x = 1, y = 2;
Swap(ref x, ref y);
Console.WriteLine($"after Swap: x = {x}, y = {y}");
// Generic math (static abstract interface members, .NET 7+)
Console.WriteLine($"SumAll<int> = {SumAll([1, 2, 3])}, SumAll<double> = {SumAll([0.5, 0.25])}, SumAll<decimal> = {SumAll([1.1m, 2.2m])}");
// Constraints
Console.WriteLine($"Create<Ecu>().Code = {Create<Ecu>().Code}");
// Covariance (out) and contravariance (in)
IEnumerable<string> names = ["VCU", "BMS"];
IEnumerable<object> objects = names; // OK: IEnumerable<out T> is covariant
Action<object> printAny = o => Console.WriteLine($" printing {o}");
Action<string> printString = printAny; // OK: Action<in T> is contravariant
printString("contrav
…Expected Result
Repository1` is the CLR's name for a generic type with one type parameter.
Get(2) = Vehicle { Id = 2, Name = Hauler }, Get(9) = null, Count = 2
Max(3, 9) = 9, Max("abc", "abd") = abd
after Swap: x = 2, y = 1
SumAll<int> = 6, SumAll<double> = 0.75, SumAll<decimal> = 3.3
Create<Ecu>().Code = default
printing contravariance works
covariance: 2 objects
Repository`1: IsGenericTypeDefinition = True; Repository`1<Vehicle>: IsGenericTypeDefinition = False
typeof(List<int>) == typeof(List<int>): True, List<int> vs List<string> same type: FalseCommon Mistakes
Violating a constraint — real compiler error:
Using object or non-generic collections (ArrayList) and casting — lose type safety and box values.
Over-constraining (requiring class or new() when not needed).
Expecting List<string> to convert to List<object> — List<T> is invariant (only interfaces/delegates can be variant).
Comparing T values with == without a constraint (use EqualityComparer<T>.Default.Equals(a, b)).
Console.WriteLine(Largest(new object(), new object()));
static T Largest<T>(T a, T b) where T : IComparable<T> => a.CompareTo(b) > 0 ? a : b;m18_err.cs(1,19): error CS0311: The type 'object' cannot be used as type parameter 'T' in the generic type or method 'Largest<T>(T, T)'. There is no implicit reference conversion from 'object' to 'System.IComparable<object>'.Best Practices
Prefer generic collections and interfaces everywhere.
Add the weakest constraint that makes the code compile.
Name multiple type parameters descriptively (TKey, TValue).
Use generic math (INumber<T>) for numeric algorithms that work on any number type.
Interview Relevance
High: "why generics (type safety, no boxing, reuse)?", "what constraints exist?", "covariance vs contravariance with examples", "default(T)", "how does the CLR implement generics?".
Practice Questions
Write a generic Pair<TFirst, TSecond> record and a Swap() method returning Pair<TSecond, TFirst>.
Write static T Clamp<T>(T value, T min, T max) where T : IComparable<T>.
Implement a generic RingBuffer<T> with fixed capacity.
Write Average<T>(IEnumerable<T>) where T : INumber<T> returning T.
Explain why IEnumerable<object> x = new List<int>(); does not compile.
Quick Quiz
Q1. (Multiple choice) Which constraint allows new T() inside a generic method? A) where T : class B) where T : new() C) where T : struct D) none
Answer: B. (struct also allows new T(), but B is the explicit constructor constraint.)
Q2. (True/False) IEnumerable<string> can be assigned to IEnumerable<object>.
Answer: True. IEnumerable<out T> is covariant (verified).
Q3. (Predict the output) Max("abc", "abd") with a.CompareTo(b) >= 0 ? a : b.
Answer: abd (verified).
Q4. (Find the bug) Largest(new object(), new object()) for Largest<T>(T a, T b) where T : IComparable<T>.
Answer: object does not implement IComparable<object> (CS0311, verified).
Coding Challenge
Implement a generic RingBuffer<T> with Add, Count and an enumerator returning items oldest-first, overwriting the oldest when full.
using System.Collections;
var rb = new RingBuffer<int>(3);
foreach (int v in new[] { 1, 2, 3, 4, 5 }) rb.Add(v);
Console.WriteLine($"{string.Join(",", rb)} (Count {rb.Count})");
class RingBuffer<T>(int capacity) : IEnumerable<T>
{
private readonly T[] _items = new T[capacity];
private int _start, _count;
public int Count => _count;
public void Add(T item)
{
_items[(_start + _count) % _items.Length] = item;
if (_count < _items.Length) _count++;
else _start = (_start + 1) % _items.Length;
}
public IEnumerator<T> GetEnumerator()
{
for (int i = 0; i < _count; i++) yield return _items[(_start + i) % _items.Length];
}
IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
}