C# Generics Explained: A Guide to Type-Safe, Reusable Code

An introduction to generics in C#. Learn how to use generic classes, methods, and interfaces to write flexible, reusable, and type-safe code that avoids casting and boxing.

One of the most powerful features in C# for creating reusable and efficient code is generics. Generics allow you to design classes and methods that defer the specification of one or more types until the class or method is declared and instantiated by client code. In simple terms, they let you write a class or method that can work with any data type in a type-safe manner.

The Problem Before Generics

Before generics were introduced in C# 2.0, if you wanted to create a collection that could hold items of any type, you had to use the System.Collections namespace, which worked with the object type.

// Using a non-generic ArrayList
var list = new System.Collections.ArrayList();
list.Add(1);          // An integer
list.Add("Hello");  // A string

// To get the integer back, you have to cast it
int myNumber = (int)list[0];

This approach has two major problems:

  1. It's not type-safe: You could accidentally add a string to a list of integers, and the compiler wouldn't stop you. You would only find the error at runtime when you tried to cast the string to an integer, resulting in an InvalidCastException.
  2. It's inefficient: For value types (like int, double, etc.), adding them to an ArrayList involves an operation called boxing (converting a value type to an object). Retrieving them requires unboxing (converting the object back to a value type). These conversions add performance overhead.

The Solution: Generics

Generics solve both of these problems. The most common generic class you'll see is List<T>.

// Using a generic List<T>
var numbers = new List<int>();
numbers.Add(1);
numbers.Add(2);

// This would cause a compile-time error, which is good!
// numbers.Add("Hello");

// No casting is needed, and no boxing/unboxing occurs.
int myNumber = numbers[0];

Here, T is the type parameter. When we create new List<int>(), we are specifying that T is int. The compiler then ensures that we can only add integers to this list, providing compile-time type safety and eliminating the performance overhead of boxing.

Creating Your Own Generic Class

You can easily create your own generic classes. Let's create a simple generic Repository class that can work with any type of entity.

public class Repository<T>
{
    private readonly List<T> _items = new List<T>();

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

    public T GetById(int index)
    {
        return _items[index];
    }
}

// --- Usage ---

// A repository for strings
var stringRepo = new Repository<string>();
stringRepo.Add("Hello");
stringRepo.Add("World");

// A repository for integers
var intRepo = new Repository<int>();
intRepo.Add(10);
intRepo.Add(20);

Creating a Generic Method

You can also create methods that are generic, even if they are in a non-generic class.

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

// --- Usage ---
var utils = new Utilities();

int x = 5, y = 10;
utils.Swap(ref x, ref y); // T is inferred as int

string s1 = "Hello", s2 = "World";
utils.Swap(ref s1, ref s2); // T is inferred as string

Constraints on Type Parameters

Sometimes, you need to ensure that the type parameter T has certain capabilities. For example, you might need to know that T is a class, has a parameterless constructor, or implements a specific interface. You can do this with constraints using the where keyword.

// We constrain T to be a class that implements the IEntity interface
// and has a parameterless constructor (new()).
public class Repository<T> where T : class, IEntity, new()
{
    public T CreateNew()
    {
        // We can call new() because of the 'new()' constraint
        T entity = new T();
        
        // We can access the Id property because of the 'IEntity' constraint
        Console.WriteLine($"Created new entity with ID: {entity.Id}");
        
        return entity;
    }
}

public interface IEntity
{
    int Id { get; set; }
}

Conclusion

Generics are a fundamental part of the C# language and a cornerstone of the .NET class library. They provide a powerful way to write code that is:

  • Reusable: Write a piece of logic once and use it for many different data types.
  • Type-Safe: Errors are caught by the compiler at development time, not by your users at runtime.
  • Efficient: Avoids the performance penalties of casting and boxing/unboxing.

By mastering generics, you can write code that is cleaner, more flexible, and more robust.