C# Generics
Master generics: write reusable, type-safe code.
By EMEPublished: February 20, 2025
csharpgenericstype safetyreusabilitytemplates
A Simple Analogy
Generics are like universal tools. A socket wrench works with different nut sizes—it's generic. Without generics, you'd need a separate wrench for each size. Generics let one class work with any type safely.
What Are Generics?
Generics allow classes, interfaces, and methods to work with different types while maintaining type safety. They eliminate casting and enable compile-time type checking.
Why Use Generics?
- Type safety: Errors caught at compile-time, not runtime
- Reusability: One implementation for many types
- No casting: Works naturally with any type
- Performance: No boxing/unboxing overhead
- Clarity: Intent is explicit
Generic Classes
// Generic repository works with any entity type
public class Repository<T> where T : class
{
private readonly List<T> _items = new();
public void Add(T item) => _items.Add(item);
public T GetById(int id) => _items.FirstOrDefault();
public List<T> GetAll() => _items.ToList();
}
// Usage
var userRepo = new Repository<User>();
userRepo.Add(new User { Name = "Alice" });
var productRepo = new Repository<Product>();
productRepo.Add(new Product { Name = "Laptop" });
Generic Methods
public class Printer
{
// Generic method works with any type
public void PrintArray<T>(T[] array)
{
foreach (var item in array)
{
Console.WriteLine(item);
}
}
}
// Usage
var printer = new Printer();
printer.PrintArray(new int[] { 1, 2, 3 }); // Prints integers
printer.PrintArray(new string[] { "a", "b", "c" }); // Prints strings
Generic Constraints
// Constraint: T must be a reference type
public class Repository<T> where T : class
{
// Valid: class, interface, delegate
// Invalid: struct, enum
}
// Constraint: T must inherit from Entity
public class EntityRepository<T> where T : Entity
{
public int GetId(T entity) => entity.Id;
}
// Multiple constraints
public class Cache<T> where T : class, IDataItem, new()
{
public T CreateDefault() => new T();
}
// Common constraint combinations
where T : class // Reference type
where T : struct // Value type
where T : IComparable // Implements interface
where T : Base // Inherits class
where T : new() // Has parameterless constructor
Practical Example
// Generic result wrapper
public class Result<T>
{
public bool IsSuccess { get; set; }
public T Data { get; set; }
public string Error { get; set; }
public static Result<T> Success(T data) =>
new() { IsSuccess = true, Data = data };
public static Result<T> Failure(string error) =>
new() { IsSuccess = false, Error = error };
}
// Usage
Result<User> userResult = await GetUserAsync(123);
if (userResult.IsSuccess)
{
Console.WriteLine(userResult.Data.Name);
}
Result<List<Order>> ordersResult = await GetOrdersAsync();
if (ordersResult.IsSuccess)
{
foreach (var order in ordersResult.Data)
{
// Process orders
}
}
Generic Interfaces
// Generic interface
public interface IRepository<T> where T : class
{
Task<T> GetByIdAsync(int id);
Task<List<T>> GetAllAsync();
Task AddAsync(T entity);
Task UpdateAsync(T entity);
Task DeleteAsync(int id);
}
// Implementation for User
public class UserRepository : IRepository<User>
{
public async Task<User> GetByIdAsync(int id) =>
await _context.Users.FindAsync(id);
public async Task<List<User>> GetAllAsync() =>
await _context.Users.ToListAsync();
// ... other methods
}
// Implementation for Product
public class ProductRepository : IRepository<Product>
{
public async Task<Product> GetByIdAsync(int id) =>
await _context.Products.FindAsync(id);
public async Task<List<Product>> GetAllAsync() =>
await _context.Products.ToListAsync();
// ... other methods
}
Covariance & Contravariance
// Covariance: IEnumerable<out T>
IEnumerable<string> strings = new List<string>();
IEnumerable<object> objects = strings; // Valid
// Contravariance: Action<in T>
Action<object> actionObject = (obj) => Console.WriteLine(obj);
Action<string> actionString = actionObject; // Valid
actionString("hello");
public interface IRepository<out T> where T : class
{
T GetById(int id);
}
Best Practices
- Use constraints: Specify what T must be
- Avoid object: Use generics instead of casting
- Generic return types: More specific than object
- Consistent naming:
T,TKey,TValueare standard - Documentation: Document generic parameter meanings
Related Concepts to Explore
- Reflection with generics
- Generic specialization
- Type erasure (how generics work internally)
- LINQ queries (use generics extensively)
Summary
Generics enable type-safe, reusable code. Master generic classes, methods, and constraints to eliminate casting and write flexible code that works with any type safely.