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 unlessT
is known to be a reference type. - You cannot use reflection to get the default value of
T
unless you usedefault(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>
andStack<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.