Introduction to Generics

Vaibhav • September 11, 2025

In earlier chapters, we explored collections like List<T> and methods that operate on specific types. But what if you want to write code that works across many types-without sacrificing type safety or performance? That’s where generics come in. Generics let you define classes, methods, and interfaces with a placeholder for the type, so your code becomes reusable, flexible, and safe.

Why Generics Matter

Imagine writing a method that swaps two values. You could write one for int, another for string, and so on. But that’s tedious and error-prone. Generics let you write it once and use it for any type:

// Generic swap method
void Swap<T>(ref T a, ref T b)
{
    T temp = a;
    a = b;
    b = temp;
}

Here, T is a type parameter. When you call Swap, the compiler replaces T with the actual type you use-like int or string. This gives you strong typing and avoids boxing/unboxing or runtime type checks.

Generics were introduced in C# 2.0 and are now a cornerstone of modern C# development. They power collections, LINQ, delegates, and even async programming.

Type Safety Without Repetition

Before generics, developers used object to write reusable code. But that meant losing type safety and needing casts:

// Non-generic version
object value = 42;
int number = (int)value; // Risky: runtime error if wrong type

With generics, the compiler enforces type correctness at compile time:

// Generic version
List<int> numbers = new List<int>();
numbers.Add(42); // Safe
int n = numbers[0]; // No cast needed

This eliminates a whole class of bugs and makes your code easier to read and maintain.

Generic Type Parameters

A generic type parameter is a placeholder for a type. You define it using angle brackets <T>. The name T is conventional, but you can use any identifier. For example:

// Generic class
public class Box<T>
{
    public T Value;
}

You can now create boxes for any type:

Box<int> intBox = new Box<int>();
intBox.Value = 123;

Box<string> strBox = new Box<string>();
strBox.Value = "Hello";

The compiler ensures that intBox only holds integers, and strBox only holds strings.

Generics in the Real World

You’ve already used generics without realizing it. The List<T> class is generic. So are Dictionary<K,V>, Queue<T>, and Stack<T>. Even LINQ methods like Select and Where are generic.

List<string> names = new List<string> { "Alice", "Bob" };
List<int> scores = new List<int> { 90, 85 };

These collections are strongly typed, so you get IntelliSense, compile-time checks, and no need for casting.

Generics are not just for collections. You can use them in your own classes, methods, interfaces, and delegates. They’re a powerful abstraction tool.

Generic Methods vs Generic Classes

You can define generics at the class level or method level. Use class-level generics when the type is part of the object’s identity. Use method-level generics when the type is only relevant to a specific operation.

// Generic method inside non-generic class
public class Utility
{
    public static void Print<T>(T item)
    {
        Console.WriteLine(item);
    }
}

This method works for any type, even though the class itself isn’t generic.

Constraints and Flexibility

Sometimes you want to restrict what types can be used with a generic. That’s where constraints come in. For example, you might require that T implements an interface or has a parameterless constructor. We’ll explore constraints in the next article.

Performance Benefits

Generics avoid boxing and unboxing for value types. That means better performance and fewer allocations. For example:

// Non-generic collection
ArrayList list = new ArrayList();
list.Add(42); // Boxed
int x = (int)list[0]; // Unboxed

// Generic collection
List<int> list2 = new List<int>();
list2.Add(42); // No boxing
int y = list2[0]; // No unboxing

This matters in performance-critical code like games, simulations, or large-scale data processing.

Prefer generics over object or non-generic collections. They’re safer, faster, and easier to maintain.

Common Pitfalls

  • Using object instead of generics: leads to casting and runtime errors.
  • Overusing generics: don’t make everything generic-use them when type flexibility is needed.
  • Ignoring constraints: without constraints, you may get compile-time errors when using certain operations.

Summary

Generics let you write reusable, type-safe code that works across many types. They eliminate the need for casting, improve performance, and make your APIs cleaner. You’ve already used generics in collections like List<T>, and now you understand how to define your own generic classes and methods. In the next article, we’ll explore Generic Classes in depth-how to design them, when to use them, and how they interact with other features like inheritance and interfaces.