Mastering C# Generics: From Beginner to Expert
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:
- Lack of Type Safety: You could accidentally add a
stringto a list ofints. - Performance overhead: “Boxing” and “Unboxing” occurred whenever a value type (like
int) was treated as anobject.
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):xis a placeholder for a value. - Generics (
List<T>):Tis 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:
T: The default choice when there is only one generic type.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.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 inIEnumerable<out T>.- Intuition: You are only reading values from the collection.
IEnumerable<string>can be treated asIEnumerable<object>.
- Contravariance (
in T): Allows you to use a less derived type. Used inIComparer<in T>.- Intuition: You are only passing values into the method.
- An
IComparer<object>can be used to comparestrings.
Generic Delegates
C# provides built-in generic delegates to avoid manual declaration:
Action<T>: A method that takesTand returnsvoid.Func<T, TResult>: A method that takesTand returnsTResult.Predicate<T>: A method that takesTand returnsbool.
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:
- Value Types (
int,struct): The JIT creates a unique copy of the machine code for each type (Specialization). This makesList<int>as fast as a raw array. - 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
- Naming Conventions: Use descriptive names if
Tisn’t enough (e.g.,TRequest,TResponse). Otherwise, use a single capitalT. - Favor Interfaces: Return
IEnumerable<T>orIReadOnlyList<T>instead ofList<T>to hide implementation details. - Minimize Constraints: Only add constraints that are absolutely necessary for your logic.
- Avoid Over-Genericizing: Don’t make everything generic just because you can. If you only ever use a class with
string, don’t make itClass<T>. - Use
defaultwisely: In generic code, usereturn 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.
Leave a comment