5 minute read

8 min read 1638 words

Generics are one of the most powerful features in C#. They allow you to write code that is decoupled from specific data types, enabling maximum code reuse, type safety, and performance. In this post, we will journey from the basics to the most advanced patterns used by experts.


1. Beginner Level: The Fundamentals

What are Generics?

Before generics (introduced in C# 2.0), if you wanted a collection of objects, you had to use ArrayList, which stored everything as object. This led to two major problems:

  1. Lack of Type Safety: You could accidentally add a string to a list of ints.
  2. Performance overhead: “Boxing” and “Unboxing” occurred whenever a value type (like int) was treated as an object.

Generics solve this by allowing you to use a Type Parameter (usually denoted as <T>).

The Intuition: “Type Variables”

If you find the syntax <T> or <TKey, TValue> confusing, try this mental model: Generics are “Type Variables”.

  • Normal Variables (int x = 5): x is a placeholder for a value.
  • Generics (List<T>): T is a placeholder for a type.

How to “Get Used” to it?

The best way is to compare them to Method Parameters:

Feature Regular Method Generic Method
Declaration void Print(string message) void Print<T>(T message)
Logic “I’ll take a string value.” “I’ll take any type, and I’ll call it T.”
Usage Print("Hello") Print<string>("Hello")

When you see <T>, just read it as: “I’m going to use a type here that I haven’t decided yet.”

When you define class Box<T>, you are telling C#: “I’m building a box. I don’t know what’s going inside yet, so I’ll just call the type ‘T’. When someone actually uses the box, they’ll tell me if ‘T’ is an int, a string, or a User.”

Decoding the Names: T, TKey, TValue

The naming follows a simple convention to make code readable:

  1. T: The default choice when there is only one generic type.
  2. TKey / TValue: Used when you have two types that play specific roles (like in a Dictionary). The “T” prefix stands for “Type”, and the rest describes its purpose.
  3. TRequest / TResponse: Common in web development to show what type of data is coming in vs. going out.

Why use these instead of real names? It prevents “Hard-coding”. If you named it string, the class would only work with strings. By naming it T, you keep it “Generic”.

Simple Generic Method

A classic example is a Swap method:

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

// Usage:
int x = 1, y = 2;
Swap(ref x, ref y); // C# infers that T is int

Simple Generic Class

A container that can hold any type:

public class Box<T> {
    public T Content { get; set; }
}

var intBox = new Box<int> { Content = 10 };
var stringBox = new Box<string> { Content = "Hello" };

2. Intermediate Level: Constraints and Logic

Multiple Type Parameters

You can define multiple placeholders, often seen in dictionaries.

public class Pair<TKey, TValue> {
    public TKey Key { get; set; }
    public TValue Value { get; set; }
}

Generic Constraints (where clause)

Sometimes you need T to have certain capabilities. Constraints restrict what types can be used as arguments.

Constraint Description
where T : struct T must be a value type.
where T : class T must be a reference type.
where T : notnull T must be a non-nullable type.
where T : new() T must have a public parameterless constructor.
where T : BaseClass T must be or derive from BaseClass.
where T : ISomeInterface T must implement the specified interface.

Example: A Repository that only works with Entities

public class Repository<T> where T : IEntity, new() {
    public T CreateDefault() => new T();
}

3. Advanced Level: Variance and Composition

Covariance and Contravariance

This is often the most confusing part of generics. It determines if you can use a more derived or less derived type than originally specified.

  • Covariance (out T): Allows you to use a more derived type. Used in IEnumerable<out T>.
    • Intuition: You are only reading values from the collection.
    • IEnumerable<string> can be treated as IEnumerable<object>.
  • Contravariance (in T): Allows you to use a less derived type. Used in IComparer<in T>.
    • Intuition: You are only passing values into the method.
    • An IComparer<object> can be used to compare strings.

Generic Delegates

C# provides built-in generic delegates to avoid manual declaration:

  • Action<T>: A method that takes T and returns void.
  • Func<T, TResult>: A method that takes T and returns TResult.
  • Predicate<T>: A method that takes T and returns bool.

4. Expert Level: High Performance and Meta-Programming

Generic Math (C# 11+)

In the past, you couldn’t use operators like + or - on generic types. With Static Abstract Members in Interfaces, we now have Generic Math.

public T AddAll<T>(IEnumerable<T> values) where T : INumber<T> {
    T sum = T.Zero;
    foreach (var val in values) {
        sum += val;
    }
    return sum;
}

How the JIT Handles Generics

The .NET Just-In-Time (JIT) compiler handles generics intelligently:

  1. Value Types (int, struct): The JIT creates a unique copy of the machine code for each type (Specialization). This makes List<int> as fast as a raw array.
  2. Reference Types (string, class): The JIT shares the same machine code for all reference types because they are all just pointers (64-bit addresses). This saves memory.

Reflection with Generics

If you are building a framework (like an ORM or Serializer), you might need to create types at runtime.

Type d1 = typeof(List<>);
Type[] typeArgs = { typeof(int) };
Type constructed = d1.MakeGenericType(typeArgs);
object list = Activator.CreateInstance(constructed);

5. Best Practices for Experts

  1. Naming Conventions: Use descriptive names if T isn’t enough (e.g., TRequest, TResponse). Otherwise, use a single capital T.
  2. Favor Interfaces: Return IEnumerable<T> or IReadOnlyList<T> instead of List<T> to hide implementation details.
  3. Minimize Constraints: Only add constraints that are absolutely necessary for your logic.
  4. Avoid Over-Genericizing: Don’t make everything generic just because you can. If you only ever use a class with string, don’t make it Class<T>.
  5. Use default wisely: In generic code, use return default; to return the appropriate “null” value for both value types (0, false) and reference types (null).

Summary

Generics are the backbone of the .NET ecosystem. Moving from a beginner (using List<T>) to an expert (writing Generic Math and understanding JIT specialization) allows you to write highly efficient, reusable, and clean C# code.

Further Reading

Leave a comment