Skip to main content

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:

csharp
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:

csharp
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

csharp
// 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:

csharp
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

csharp
// 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:

csharp
// 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:

ConstraintDescription
where T : structT must be a value type
where T : classT must be a reference type
where T : new()T must have a public parameterless constructor
where T : BaseClassT must be or inherit from BaseClass
where T : IInterfaceT must implement IInterface

Example with constraints:

csharp
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:

csharp
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:

csharp
// 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:

  1. Type Safety: Generics catch type errors at compile time, not runtime
  2. No Boxing/Unboxing: Better performance with value types
  3. No Type Casting Required: Code is cleaner and safer
  4. Intellisense Support: Better development experience

Generic Methods in Non-Generic Classes

You can also define generic methods within non-generic classes:

csharp
public class Utilities
{
public void DisplayArray<T>(T[] array)
{
foreach (T item in array)
{
Console.Write($"{item} ");
}
Console.WriteLine();
}
}

Usage:

csharp
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

  1. Create a generic Stack<T> class that implements Push, Pop, and Peek methods
  2. Build a generic Queue<T> class with Enqueue, Dequeue, and Peek methods
  3. Create a generic KeyValuePair<TKey, TValue> class
  4. Implement a generic CircularBuffer<T> class with a fixed capacity
  5. 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! :)