C# Generic Classes
When you're building applications, you often need to create classes that can work with different data types. Instead of writing multiple versions of the same class or relying on objects that require type casting, C# provides a powerful feature called generic classes that enables you to write flexible, reusable, and type-safe code.
What Are Generic Classes?
Generic classes are classes that can operate on data of any type while maintaining type safety. They allow you to define a class template where the data type is specified later when the class is instantiated.
Think of a generic class as a blueprint with placeholders for data types that get filled in when you use the class.
Basic Syntax
Here's the basic syntax for declaring a generic class:
public class ClassName<T>
{
// Class members that can use type T
}
Where:
T
is a type parameter (a placeholder for a specific type)- You can use
T
throughout the class to represent the type that will be specified when the class is instantiated
Creating Your First Generic Class
Let's create a simple generic class that can store and retrieve a value of any type:
public class Box<T>
{
private T item;
public void Store(T item)
{
this.item = item;
}
public T Retrieve()
{
return item;
}
}
How to Use This Generic Class
// Using the Box class with an integer
Box<int> intBox = new Box<int>();
intBox.Store(123);
int number = intBox.Retrieve();
Console.WriteLine($"Retrieved integer: {number}");
// Using the Box class with a string
Box<string> stringBox = new Box<string>();
stringBox.Store("Hello Generics!");
string message = stringBox.Retrieve();
Console.WriteLine($"Retrieved string: {message}");
Output:
Retrieved integer: 123
Retrieved string: Hello Generics!
Multiple Type Parameters
Generic classes can have multiple type parameters:
public class Pair<TFirst, TSecond>
{
public TFirst First { get; set; }
public TSecond Second { get; set; }
public Pair(TFirst first, TSecond second)
{
First = first;
Second = second;
}
public override string ToString()
{
return $"({First}, {Second})";
}
}
Using Multiple Type Parameters
// Name and age pair
Pair<string, int> person = new Pair<string, int>("John", 25);
Console.WriteLine($"Person: {person}");
// City and population
Pair<string, double> city = new Pair<string, double>("New York", 8.4);
Console.WriteLine($"City: {city}");
Output:
Person: (John, 25)
City: (New York, 8.4)
Generic Constraints
Sometimes you need to restrict the types that can be used with your generic class. You can do this using constraints:
// Class must be non-abstract with a parameterless constructor
public class Factory<T> where T : class, new()
{
public T Create()
{
return new T();
}
}
Common constraints include:
Constraint | Description |
---|---|
where T : struct | T must be a value type |
where T : class | T must be a reference type |
where T : new() | T must have a public parameterless constructor |
where T : BaseClass | T must be or inherit from BaseClass |
where T : IInterface | T must implement IInterface |
Example with constraints:
public class EntityProcessor<T> where T : class, IEntity, new()
{
public void Process(string id)
{
T entity = new T();
entity.Id = id;
entity.Process();
}
}
public interface IEntity
{
string Id { get; set; }
void Process();
}
public class Customer : IEntity
{
public string Id { get; set; }
public string Name { get; set; }
public void Process()
{
Console.WriteLine($"Processing customer {Id}");
}
}
Real-World Example: Generic Repository
A common pattern in application development is the repository pattern. Generic classes are perfect for this:
public interface IRepository<T> where T : class
{
List<T> GetAll();
T GetById(int id);
void Add(T entity);
void Update(T entity);
void Delete(int id);
}
public class Repository<T> : IRepository<T> where T : class
{
private readonly List<T> _entities = new List<T>();
private int _nextId = 1;
public List<T> GetAll()
{
return _entities;
}
public T GetById(int id)
{
// Simplified implementation
return _entities.FirstOrDefault();
}
public void Add(T entity)
{
_entities.Add(entity);
}
public void Update(T entity)
{
// Implementation would depend on actual entities
Console.WriteLine("Entity updated");
}
public void Delete(int id)
{
// Implementation would remove by id
Console.WriteLine($"Entity with id {id} deleted");
}
}
This generic repository can work with any class type, making it highly reusable across your application.
Generic Classes vs. Object Type
You might wonder why not just use the object
type instead of generics:
// Using object (non-generic)
public class NonGenericBox
{
private object item;
public void Store(object item)
{
this.item = item;
}
public object Retrieve()
{
return item;
}
}
// Using the non-generic box
NonGenericBox box = new NonGenericBox();
box.Store(123);
// Requires explicit casting, potential for runtime errors
int number = (int)box.Retrieve();
Advantages of generic classes over using object:
- Type Safety: Generics catch type errors at compile time, not runtime
- No Boxing/Unboxing: Better performance with value types
- No Type Casting Required: Code is cleaner and safer
- Intellisense Support: Better development experience
Generic Methods in Non-Generic Classes
You can also define generic methods within non-generic classes:
public class Utilities
{
public void DisplayArray<T>(T[] array)
{
foreach (T item in array)
{
Console.Write($"{item} ");
}
Console.WriteLine();
}
}
Usage:
Utilities utils = new Utilities();
int[] numbers = { 1, 2, 3, 4, 5 };
string[] names = { "Alice", "Bob", "Charlie" };
utils.DisplayArray(numbers);
utils.DisplayArray(names);
Output:
1 2 3 4 5
Alice Bob Charlie
Summary
Generic classes in C# allow you to:
- Create reusable code that works with any data type
- Maintain type safety without sacrificing flexibility
- Improve performance by avoiding boxing/unboxing and type casting
- Build more robust applications with fewer errors
By mastering generic classes, you'll write cleaner, more efficient code and develop reusable components that can adapt to different data types while maintaining the safety of strong typing.
Exercises
- Create a generic
Stack<T>
class that implementsPush
,Pop
, andPeek
methods - Build a generic
Queue<T>
class withEnqueue
,Dequeue
, andPeek
methods - Create a generic
KeyValuePair<TKey, TValue>
class - Implement a generic
CircularBuffer<T>
class with a fixed capacity - Build a generic
Calculator<T>
class with constraints to ensure it works only with numeric types
Additional Resources
If you spot any mistakes on this website, please let me know at [email protected]. I’d greatly appreciate your feedback! :)