Generic Classes

Vaibhav • September 11, 2025

In the previous article, we introduced the concept of generics-how they allow us to write reusable, type-safe code that works across many types. Now it’s time to explore one of the most powerful applications of generics: generic classes. These are classes that operate on a placeholder type, letting you build flexible data structures, utilities, and APIs without sacrificing compile-time safety.

What Is a Generic Class?

A generic class is a class that takes a type parameter. This parameter acts as a placeholder for the actual type that will be used when the class is instantiated. You define it using angle brackets <T>, where T is the type parameter.

public class Box<T>
{
    public T Value;

    public void Display()
    {
        Console.WriteLine($"Value: {Value}");
    }
}

In this example, Box<T> is a generic class. The type T is used for the field Value and can be any type-int, string, List<int>, and so on.

Instantiating Generic Classes

When you create an object of a generic class, you specify the actual type to use in place of the type parameter. This is called type instantiation.

Box<int> intBox = new Box<int>();
intBox.Value = 42;
intBox.Display(); // Output: Value: 42

Box<string> strBox = new Box<string>();
strBox.Value = "Hello";
strBox.Display(); // Output: Value: Hello

The compiler replaces T with the specified type and enforces type safety. You cannot assign a string to intBox.Value or an integer to strBox.Value.

You can use multiple type parameters in a generic class. For example, Dictionary<TKey, TValue> uses two.

Why Use Generic Classes?

Generic classes are ideal when the behavior of the class is independent of the specific type it operates on. They help you:

  • Write reusable code without duplicating logic for each type.
  • Maintain type safety-errors are caught at compile time.
  • Improve performance by avoiding boxing/unboxing for value types.

Consider a simple stack implementation. Without generics, you’d need separate classes for int, string, etc. With generics, one class handles them all.

Building a Generic Stack

Let’s build a basic stack using a generic class. A stack is a data structure that follows Last-In-First-Out (LIFO) ordering.

public class Stack<T>
{
    private List<T> items = new List<T>();

    public void Push(T item)
    {
        items.Add(item);
    }

    public T Pop()
    {
        if (items.Count == 0)
            throw new InvalidOperationException("Stack is empty");

        T value = items[^1]; // ^1 means last item
        items.RemoveAt(items.Count - 1);
        return value;
    }

    public int Count => items.Count;
}

This stack works for any type. You can push and pop integers, strings, or even custom objects.

Stack<int> numberStack = new Stack<int>();
numberStack.Push(10);
numberStack.Push(20);
Console.WriteLine(numberStack.Pop()); // Output: 20

Stack<string> wordStack = new Stack<string>();
wordStack.Push("Hello");
wordStack.Push("World");
Console.WriteLine(wordStack.Pop()); // Output: World

The generic stack ensures that only the correct type can be pushed or popped, and it avoids runtime casting errors.

Generic Classes and Inheritance

Generic classes can inherit from other classes-generic or non-generic. You can also create generic base classes and derive from them with specific types.

public class Repository<T>
{
    public List<T> Items = new List<T>();

    public void Add(T item)
    {
        Items.Add(item);
    }
}

public class UserRepository : Repository<User>
{
    public User FindByName(string name)
    {
        return Items.FirstOrDefault(u => u.Name == name);
    }
}

Here, UserRepository inherits from a generic base class Repository<T> and specializes it for the User type.

You can also create generic interfaces and implement them in generic classes. We’ll explore that in a later article.

Type Parameter Naming

While T is the most common name for a type parameter, you can use more descriptive names when appropriate. For example:

public class Pair<TKey, TValue>
{
    public TKey Key;
    public TValue Value;
}

This makes the intent clearer, especially when working with multiple type parameters.

Limitations of Generic Classes

While generic classes are powerful, they have some limitations:

  • You cannot use operators like +, <, or == on type parameters unless you constrain them.
  • You cannot create arrays of a generic type directly: new T[10] is not allowed unless T is known to be a reference type.
  • You cannot use reflection to get the default value of T unless you use default(T).

These limitations exist because the compiler doesn’t know what T will be at runtime unless constrained. We’ll cover constraints in the next article.

Best Practices for Generic Classes

When designing generic classes, keep these principles in mind:

  • Use generics when the logic is independent of the type.
  • Keep the class focused-don’t mix unrelated responsibilities.
  • Use meaningful type parameter names when there are multiple.
  • Document the expected behavior and constraints clearly.

If your class only works with reference types or types that implement a specific interface, use constraints to enforce that. This improves clarity and prevents misuse.

Real-World Examples

Many .NET Framework classes are generic. Here are a few you’ve likely used:

  • List<T> - dynamic array with type safety.
  • Dictionary<TKey, TValue> - key-value store.
  • Queue<T> and Stack<T> - FIFO and LIFO collections.
  • Nullable<T> - allows value types to be null.

These classes are foundational to C# development and demonstrate the power of generics in building robust, reusable APIs.

Summary

Generic classes let you write flexible, reusable code that works across many types while maintaining type safety. They’re ideal for data structures, utilities, and APIs where the logic is independent of the specific type. You’ve learned how to define, instantiate, and use generic classes, and seen how they integrate with inheritance and real-world patterns. In the next article, we’ll explore Generic Methods-how to write methods that are generic even inside non-generic classes, and how to use them effectively.