C# Generics
Why Generics
Generics enable type-safe, reusable code without sacrificing performance.
// Without generics - type unsafe, boxing overhead
ArrayList list = new ArrayList();
list.Add(1);
list.Add("string"); // No compile error
int value = (int)list[0]; // Runtime cast needed
int error = (int)list[1]; // Runtime exception
// With generics - type safe, no boxing
List<int> list = new List<int>();
list.Add(1);
// list.Add("string"); // Compile error
int value = list[0]; // No cast needed
Generic Classes
Basic Generic Class
public class Box<T>
{
private T content;
public void Pack(T item)
{
content = item;
}
public T Unpack()
{
return content;
}
}
// Usage - type argument inferred or explicit
var intBox = new Box<int>();
intBox.Pack(42);
int value = intBox.Unpack();
Box<string> stringBox = new Box<string>();
stringBox.Pack("hello");
Multiple Type Parameters
public class Pair<TFirst, TSecond>
{
public TFirst First { get; set; }
public TSecond Second { get; set; }
public Pair(TFirst first, TSecond second)
{
First = first;
Second = second;
}
public void Deconstruct(out TFirst first, out TSecond second)
{
first = First;
second = Second;
}
}
var pair = new Pair<string, int>("age", 30);
var (key, value) = pair;
// Generic key-value store
public class Repository<TKey, TEntity>
where TKey : notnull
where TEntity : class
{
private readonly Dictionary<TKey, TEntity> store = new();
public void Add(TKey key, TEntity entity) => store[key] = entity;
public TEntity? Get(TKey key) => store.GetValueOrDefault(key);
}
Generic Methods
Methods can be generic independently of their containing class.
public class Utilities
{
// Generic method
public static T Max<T>(T a, T b) where T : IComparable<T>
{
return a.CompareTo(b) >= 0 ? a : b;
}
// Generic swap
public static void Swap<T>(ref T a, ref T b)
{
T temp = a;
a = b;
b = temp;
}
// Type inference
public static List<T> ToList<T>(params T[] items)
{
return new List<T>(items);
}
}
// Usage - type often inferred
int max = Utilities.Max(10, 20); // T inferred as int
string maxStr = Utilities.Max("apple", "banana");
int x = 1, y = 2;
Utilities.Swap(ref x, ref y);
var list = Utilities.ToList(1, 2, 3); // T inferred as int
Generic Extension Methods
public static class EnumerableExtensions
{
public static bool IsEmpty<T>(this IEnumerable<T> source)
{
return !source.Any();
}
public static T? FirstOrNull<T>(this IEnumerable<T> source)
where T : class
{
return source.FirstOrDefault();
}
public static IEnumerable<T> WhereNotNull<T>(this IEnumerable<T?> source)
where T : class
{
foreach (var item in source)
{
if (item is not null)
yield return item;
}
}
}
// Usage
var items = new List<int> { 1, 2, 3 };
bool empty = items.IsEmpty();
var people = new List<Person?> { person1, null, person2 };
var validPeople = people.WhereNotNull();
Type Constraints
Constraints restrict which types can be used as type arguments.
Reference Type Constraint
public class ReferenceOnlyContainer<T> where T : class
{
private T? item;
public void Set(T value) => item = value;
public T? Get() => item;
}
// Valid
var container = new ReferenceOnlyContainer<string>();
// Invalid - int is a value type
// var invalid = new ReferenceOnlyContainer<int>();
Value Type Constraint
public class ValueOnlyContainer<T> where T : struct
{
private T item;
public void Set(T value) => item = value;
public T Get() => item;
}
// Valid
var container = new ValueOnlyContainer<int>();
// Invalid - string is a reference type
// var invalid = new ValueOnlyContainer<string>();
Constructor Constraint
public class Factory<T> where T : new()
{
public T Create() => new T();
}
// Valid
var factory = new Factory<List<int>>();
var list = factory.Create();
// Invalid - string has no parameterless constructor
// var invalid = new Factory<string>();
Interface and Base Class Constraints
public class ComparableContainer<T> where T : IComparable<T>
{
private T value;
public bool IsGreaterThan(T other) => value.CompareTo(other) > 0;
}
public class AnimalShelter<T> where T : Animal
{
private List<T> animals = new();
public void Accept(T animal) => animals.Add(animal);
public IEnumerable<T> GetAll() => animals;
}
Multiple Constraints
public class Repository<TKey, TEntity>
where TKey : notnull, IComparable<TKey>
where TEntity : class, IEntity, new()
{
private SortedDictionary<TKey, TEntity> store = new();
public TEntity GetOrCreate(TKey key)
{
if (!store.TryGetValue(key, out var entity))
{
entity = new TEntity();
store[key] = entity;
}
return entity;
}
}
notnull Constraint (C# 8.0)
public class Dictionary<TKey, TValue>
where TKey : notnull // Cannot use nullable type
{
// TKey guaranteed non-null
}
unmanaged Constraint
For types that can be used in unsafe code (no reference type fields).
public class Buffer<T> where T : unmanaged
{
private T* pointer;
private int length;
public unsafe Buffer(int size)
{
pointer = (T*)Marshal.AllocHGlobal(size * sizeof(T));
length = size;
}
}
// Valid: int, double, structs with only unmanaged fields
var intBuffer = new Buffer<int>(100);
Generic Interfaces
public interface IRepository<T>
{
T? GetById(int id);
IEnumerable<T> GetAll();
void Add(T entity);
void Update(T entity);
void Delete(int id);
}
public class CustomerRepository : IRepository<Customer>
{
private readonly List<Customer> customers = new();
public Customer? GetById(int id) =>
customers.FirstOrDefault(c => c.Id == id);
public IEnumerable<Customer> GetAll() => customers;
public void Add(Customer entity) => customers.Add(entity);
public void Update(Customer entity)
{
var index = customers.FindIndex(c => c.Id == entity.Id);
if (index >= 0) customers[index] = entity;
}
public void Delete(int id) =>
customers.RemoveAll(c => c.Id == id);
}
Covariance and Contravariance
Variance allows more flexible type relationships in generic interfaces.
Covariance (out)
Return type can be more derived.
public interface IReadOnlyRepository<out T>
{
T GetById(int id);
IEnumerable<T> GetAll();
}
IReadOnlyRepository<Animal> animals = new DogRepository();
// OK because Dog is-a Animal, and we only read (out)
public interface IEnumerable<out T>
{
// T only appears in output positions
}
IEnumerable<Animal> animals = new List<Dog>(); // Valid
Contravariance (in)
Parameter type can be more general.
public interface IComparer<in T>
{
int Compare(T x, T y);
}
IComparer<Dog> dogComparer = new AnimalComparer();
// OK because AnimalComparer can compare any Animal, including Dogs
public interface IEqualityComparer<in T>
{
bool Equals(T x, T y);
int GetHashCode(T obj);
}
IEqualityComparer<string> stringComparer = new ObjectComparer();
Combined Variance
public interface IConverter<in TInput, out TOutput>
{
TOutput Convert(TInput input);
}
IConverter<Animal, string> converter = new DogToStringConverter();
// DogToStringConverter can accept Dog (more specific) for Animal
// and return string
// Func delegate is contravariant in input, covariant in output
Func<Animal, string> func = (Dog d) => d.Name; // Valid
Default Values
public class Container<T>
{
private T item = default!;
public void Reset()
{
item = default!; // null for reference, 0 for value types
}
public T GetOrDefault(Func<T> factory)
{
return item ?? factory();
}
}
// default keyword
T defaultValue = default; // Type inferred
T defaultValue2 = default(T);
Static Members in Generics
Each closed generic type has its own static members.
public class Counter<T>
{
private static int count = 0;
public Counter()
{
count++;
}
public static int Count => count;
}
var intCounter1 = new Counter<int>();
var intCounter2 = new Counter<int>();
var stringCounter = new Counter<string>();
Console.WriteLine(Counter<int>.Count); // 2
Console.WriteLine(Counter<string>.Count); // 1
Static Abstract Members (C# 11)
Allow static members in interfaces.
public interface IAdditionOperators<TSelf, TOther, TResult>
where TSelf : IAdditionOperators<TSelf, TOther, TResult>
{
static abstract TResult operator +(TSelf left, TOther right);
}
public interface INumber<T> where T : INumber<T>
{
static abstract T Zero { get; }
static abstract T One { get; }
static abstract T operator +(T left, T right);
static abstract T operator *(T left, T right);
}
// Generic math
public static T Sum<T>(IEnumerable<T> values) where T : INumber<T>
{
T result = T.Zero;
foreach (var value in values)
{
result = result + value;
}
return result;
}
Common Generic Patterns
Generic Factory
public interface IFactory<T>
{
T Create();
}
public class Factory<T> : IFactory<T> where T : new()
{
public T Create() => new T();
}
// With dependency injection
public class ServiceFactory<T> where T : class
{
private readonly IServiceProvider provider;
public ServiceFactory(IServiceProvider provider)
{
this.provider = provider;
}
public T Create() => provider.GetRequiredService<T>();
}
Generic Result Type
public class Result<T>
{
public bool IsSuccess { get; }
public T? Value { get; }
public string? Error { get; }
private Result(T value)
{
IsSuccess = true;
Value = value;
}
private Result(string error)
{
IsSuccess = false;
Error = error;
}
public static Result<T> Success(T value) => new(value);
public static Result<T> Failure(string error) => new(error);
public TResult Match<TResult>(
Func<T, TResult> success,
Func<string, TResult> failure)
{
return IsSuccess ? success(Value!) : failure(Error!);
}
}
// Usage
Result<Customer> GetCustomer(int id)
{
var customer = repository.Find(id);
return customer != null
? Result<Customer>.Success(customer)
: Result<Customer>.Failure($"Customer {id} not found");
}
Generic Specifications
public interface ISpecification<T>
{
bool IsSatisfiedBy(T entity);
}
public class AndSpecification<T> : ISpecification<T>
{
private readonly ISpecification<T> left;
private readonly ISpecification<T> right;
public AndSpecification(ISpecification<T> left, ISpecification<T> right)
{
this.left = left;
this.right = right;
}
public bool IsSatisfiedBy(T entity) =>
left.IsSatisfiedBy(entity) && right.IsSatisfiedBy(entity);
}
// Usage
public class ActiveCustomerSpec : ISpecification<Customer>
{
public bool IsSatisfiedBy(Customer customer) => customer.IsActive;
}
public class PremiumCustomerSpec : ISpecification<Customer>
{
public bool IsSatisfiedBy(Customer customer) => customer.TotalSpent > 1000;
}
var spec = new AndSpecification<Customer>(
new ActiveCustomerSpec(),
new PremiumCustomerSpec());
var premiumActive = customers.Where(c => spec.IsSatisfiedBy(c));
Version History
| Feature | Version | Significance |
|---|---|---|
| Generics | C# 2.0 | Type-safe reusable code |
| Variance | C# 4.0 | Covariance/contravariance |
| notnull constraint | C# 8.0 | Null safety |
| unmanaged constraint | C# 7.3 | Pointer operations |
| Default constraint | C# 7.3 | Allow default(T) pattern |
| Static abstract members | C# 11 | Generic math |
Key Takeaways
Use generics for type-safe reusability: Avoid object-based collections and casts.
Apply constraints to enable operations: Constraints let you call methods on type parameters.
Understand variance for flexibility: Covariance (out) for return types, contravariance (in) for parameters.
Avoid runtime type checks in generics: If you need if (typeof(T) == typeof(int)), reconsider your design.
Static abstract members enable generic math: C# 11 allows truly generic numeric algorithms.
Generic methods over generic classes: When only one method needs the type parameter, make the method generic instead of the class.
Found this guide helpful? Share it with your team:
Share on LinkedIn