Generics
C# generics give type-safe reusable code: List<T>, Dictionary<K, V>, your own generic types. Compile-time checked; CLR specialises per value-type T; no boxing.
Generic classes, methods, constraints
EXAMPLE
using System;
using System.Collections.Generic;
// 1) Generic class
public class Stack<T>
{
private readonly List<T> items = new();
public int Count => items.Count;
public void Push(T item) => items.Add(item);
public T Pop()
{
if (items.Count == 0) throw new InvalidOperationException();
var top = items[^1];
items.RemoveAt(items.Count - 1);
return top;
}
public T Peek() => items[^1];
}
var nums = new Stack<int>();
nums.Push(1);
nums.Push(2);
nums.Pop(); // 2
var names = new Stack<string>();
names.Push("Ada");
// 2) Generic method (independent of the class)
public static T First<T>(IEnumerable<T> items)
{
foreach (var item in items) return item;
throw new InvalidOperationException();
}
var f = First(new[] { 10, 20, 30 }); // 10
// 3) Multiple type parameters
public class Pair<TFirst, TSecond>
{
public TFirst First { get; set; } = default!;
public TSecond Second { get; set; } = default!;
}
var p = new Pair<string, int> { First = "Ada", Second = 32 };
// 4) Constraints — restrict T
public static T Max<T>(T a, T b) where T : IComparable<T>
{
return a.CompareTo(b) > 0 ? a : b;
}
Max(3, 7); // 7
Max("apple", "banana");
// Multiple constraints
public static T Build<T>() where T : class, new() // ref type AND has parameterless ctor
{
return new T();
}
var list = Build<List<int>>();
// Common constraint clauses:
// where T : struct — value type
// where T : class — reference type
// where T : new() — has a parameterless ctor
// where T : SomeBase — derives from SomeBase
// where T : ISomeInterface — implements ISomeInterface
// where T : notnull — cannot be null
// where T : unmanaged — unmanaged value type
// where T : U — T derives from another type param U
// 5) Generic interface
public interface IRepository<T, TId>
where T : class
where TId: IEquatable<TId>
{
Task<T?> FindAsync(TId id);
Task<List<T>> AllAsync();
Task<T> SaveAsync(T entity);
Task<bool> DeleteAsync(TId id);
}
// 6) Covariance + contravariance (in / out modifiers)
public interface IProducer<out T> // covariant — can return T
{
T Get();
}
public interface IConsumer<in T> // contravariant — can accept T
{
void Send(T item);
}
IProducer<Animal> animals = new ProducerImpl<Dog>(); // OK — Dog producer is an Animal producer
IConsumer<Dog> dogs = new ConsumerImpl<Animal>(); // OK — accepts any Dog if it accepts any Animal
// 7) Default values for T
public class Cache<T>
{
private T value = default!; // default(T) — null for ref types, 0/false/etc. for value types
}
// 8) Generic delegates
public delegate TResult Transformer<T, TResult>(T input);
Transformer<int, string> intToString = n => n.ToString();
intToString(42); // "42"
// Standard library delegates — already generic
// Action<T1, ...> : void f(T1, ...)
// Func<T1, ..., TResult> : TResult f(T1, ...)
// Predicate<T> : bool f(T)
// Comparison<T> : int f(T, T)
// 9) Generic methods can infer T
void Print<T>(T value) => Console.WriteLine(value);
Print(42); // T inferred as int
Print("hello"); // T inferred as string
Print<int>(42); // explicit
// 10) Generic + LINQ — most C# generics you use
List<User> users = ...;
var emails = users.Select(u => u.Email).Distinct().ToList();
var admins = users.Where(u => u.Role == "admin").ToList();
// 11) Real-world generic types
// List<T>, Dictionary<K, V>, HashSet<T>, Queue<T>, Stack<T>, LinkedList<T>
// Nullable<T> (T?)
// Lazy<T>
// Task<T>, IAsyncEnumerable<T>
// IEnumerable<T>, IReadOnlyList<T>, IReadOnlyDictionary<K, V>
// Tuple<T1, ...>, ValueTuple<T1, ...>
// Func<T, R>, Action<T>, Predicate<T>
// Result<TOk, TErr> (custom — usually defined per project)
// 12) Generic + record
public record Result<TOk, TErr>(TOk? Value, TErr? Error)
{
public bool IsOk => Error is null;
}
// 13) Where to use generics
// ✅ Collections / containers
// ✅ DAO / Repository pattern
// ✅ Functional helpers (Map, Filter, Reduce alternatives)
// ✅ Async results (Task<T>, AsyncEnumerable<T>)
// ✅ Generic event payloads (EventArgs<T>)
// 14) Where NOT to use generics
// ❌ When T only ever has 1-2 concrete types — just write 2 methods
// ❌ When constraints get out of hand (5+ where clauses) — refactor
// ❌ When you just want polymorphism — use an interface
// 15) Generic math (C# 11+) — static abstract members in interfaces
public static T Sum<T>(IEnumerable<T> items) where T : INumber<T>
{
var sum = T.Zero;
foreach (var x in items) sum += x;
return sum;
}
Sum(new[] { 1, 2, 3 }); // 6
Sum(new[] { 1.5, 2.5 }); // 4.0
// 16) Common bugs
// • Forgetting constraints → compiler can't infer behaviour, errors on operations
// • Using default(T) on non-nullable ref types without #nullable annotations → CS8601
// • Covariance / contravariance on mutable types → write-time errors
// • Capturing T in lambdas without closure-safe access
// • Generic + dynamic — compile-time generics don't see runtime types
// 17) Tips
// • Use generic interfaces (IRepository<T, ID>) for cross-cutting infrastructure
// • Constraints are documentation — code is much clearer when they're explicit
// • Prefer Func<T, R> / Action<T> over custom delegates when possible
// • Don't ship 'AnyType' generics (T = object); usually a sign of bad design
// • Use 'in' / 'out' modifiers for variance when the API allows
Why it matters
C# generics are zero-cost and type-safe; the CLR specialises for value types so there’s no boxing. Pair with constraints (where T : IComparable<T>) to keep the API honest — the compiler then guides everyone who calls into your code.
Tip: Tweak the snippet with Try it Yourself », then sit the quiz at the bottom of the page.
Example
Example
public class Box<T> {
public T Item { get; set; }
}
var b = new Box<int> { Item = 42 };
Try it Yourself »
Discussion
Loading…