Generics

Generics let you write type-safe code that works with any data type, without sacrificing performance or safety. They eliminate the need for casting and boxing.

Generic Class

public class Repository<T> where T : class, new()
{
    private readonly List<T> items = new();

    public void Add(T item) => items.Add(item);

    public T? GetById(int index)
    {
        if (index < 0 || index >= items.Count)
            return null;
        return items[index];
    }

    public IReadOnlyList<T> GetAll() => items.AsReadOnly();

    public int Count => items.Count;
}

// Usage
var repo = new Repository<Product>();
repo.Add(new Product { Name = "Widget" });
repo.Add(new Product { Name = "Gadget" });

Product? item = repo.GetById(0);
Console.WriteLine(item?.Name); // Widget

Generic Methods

public static T Max<T>(T a, T b) where T : IComparable<T>
{
    return a.CompareTo(b) >= 0 ? a : b;
}

Console.WriteLine(Max(3, 7));       // 7
Console.WriteLine(Max("apple", "banana")); // banana

Constraints

Constraints restrict what types can be used with a generic:

where T : struct          // value type only
where T : class           // reference type only
where T : new()           // must have parameterless constructor
where T : IComparable<T>  // must implement interface
where T : BaseClass       // must derive from class
where T : notnull         // cannot be null

Built-in Generic Collections

You use generics every day in C#:

List<string> names = new();
Dictionary<string, int> scores = new();
Queue<Task> tasks = new();
Stack<int> history = new();
HashSet<string> unique = new();

Why Not Use object?

Before generics, collections stored object, requiring casts and boxing:

// Old way — boxing, no type safety
ArrayList list = new();
list.Add(42);           // boxed to object
int n = (int)list[0];   // must cast back

// Generic way — type safe, no boxing
List<int> list = new() { 42 };
int n = list[0];        // no cast needed