C# Generics

Generics provide a way to create programming elements that operate on a variety of types rather than a single, fixed type. Think of them as placeholders for types that are specified when the generic type is used.

Tip: Generics improve code reusability and type safety. They allow you to define classes, interfaces, methods, and delegates that can work with any type while maintaining compile-time type checking.

What are Generics?

Before generics, if you wanted to create a collection that could hold any type of object, you would typically use the base object type. This worked, but it had drawbacks:

Generics solve these problems by allowing you to specify the type parameter at the time you declare or instantiate a generic type.

Generic Classes

A generic class is a class that is parameterized by one or more type parameters. The most common example is the generic List<T> class in the .NET Framework.

Defining a Generic Class

Here's a simple generic class for a stack:


public class GenericStack<T>
{
    private T[] _stack;
    private int _top;

    public GenericStack(int capacity = 10)
    {
        _stack = new T[capacity];
        _top = -1;
    }

    public void Push(T item)
    {
        if (_top == _stack.Length - 1)
        {
            // Handle stack overflow, e.g., resize array
            throw new StackOverflowException("Stack is full.");
        }
        _stack[++_top] = item;
    }

    public T Pop()
    {
        if (_top < 0)
        {
            throw new InvalidOperationException("Stack is empty.");
        }
        T item = _stack[_top--];
        _stack[_top + 1] = default(T); // Optional: Clear reference
        return item;
    }

    public T Peek()
    {
        if (_top < 0)
        {
            throw new InvalidOperationException("Stack is empty.");
        }
        return _stack[_top];
    }

    public bool IsEmpty => _top == -1;
    public int Count => _top + 1;
}
            

Using a Generic Class

You specify the type parameter when you create an instance of the generic class:


// Stack for integers
GenericStack<int> intStack = new GenericStack<int>(5);
intStack.Push(10);
intStack.Push(20);
int poppedInt = intStack.Pop(); // poppedInt will be 20

// Stack for strings
GenericStack<string> stringStack = new GenericStack<string>();
stringStack.Push("Hello");
stringStack.Push("World");
string poppedString = stringStack.Pop(); // poppedString will be "World"

// This would cause a compile-time error:
// intStack.Push("This is not an int");
            

Generic Interfaces

Generic interfaces are interfaces that are parameterized by one or more type parameters.

Defining a Generic Interface


public interface IRepository<T>
{
    void Add(T item);
    T GetById(int id);
    IEnumerable<T> GetAll();
    void Update(T item);
    void Delete(int id);
}
            

Implementing a Generic Interface


public class CustomerRepository : IRepository<Customer>
{
    // ... implementation details for Customer
    public void Add(Customer item) { /* ... */ }
    public Customer GetById(int id) { /* ... */ return null; }
    public IEnumerable<Customer> GetAll() { /* ... */ return new List<Customer>(); }
    public void Update(Customer item) { /* ... */ }
    public void Delete(int id) { /* ... */ }
}
            

Generic Methods

You can also define generic methods, which are methods that are parameterized by one or more type parameters.

Defining a Generic Method


public static class UtilityHelper
{
    public static void Swap<T>(ref T a, ref T b)
    {
        T temp = a;
        a = b;
        b = temp;
    }

    public static T Copy<T>(T source) where T : ICloneable
    {
        return (T)source.Clone();
    }
}
            

Using a Generic Method


int x = 5;
int y = 10;
UtilityHelper.Swap(ref x, ref y); // x is now 10, y is now 5

string s1 = "apple";
string s2 = "banana";
UtilityHelper.Swap(ref s1, ref s2); // s1 is now "banana", s2 is now "apple"

// Using the Copy method (assuming Customer implements ICloneable)
// Customer originalCustomer = GetCustomer();
// Customer copiedCustomer = UtilityHelper.Copy(originalCustomer);
            

Constraints

Constraints are used to restrict the types that can be used as type arguments for a generic type or method. This allows you to use certain operations on the type parameters that might not be available for arbitrary types.

Common constraints include:

Note: The new() constraint must be specified last.

Benefits of Generics

Common Generic Collections

The .NET Framework provides a rich set of generic collections in the System.Collections.Generic namespace, such as:

Familiarizing yourself with these collections is crucial for efficient data management in C#.