Generics
Write classes and methods that work with any type while staying fully type-safe.
12 min read
You will learn
- Use generic types like List<T> and Dictionary<TKey, TValue>
- Write generic methods and generic classes
- Restrict type parameters with constraints
- Explain why generics beat object
The problem: the same code for every type
Suppose you write a method to find the largest number in an array of int. Then you need it for double. Then for decimal, string, DateTime… Copying the method for each type is clearly wrong. Using object for everything loses type safety:
object Largest(object[] items) { ... }
var result = Largest(new object[] { 3, 9, 2 });
int x = (int)result; // cast needed — and it crashes if you guessed the type wrongGenerics let you write the code once, with a type parameter, and use it with any type — while the compiler still checks everything.
You already use generics
List<string> names = new();
Dictionary<string, int> ages = new();
Task<int> pending = ...;List<T> is defined once, with a placeholder T. List<string> fills in T = string: its Add method only accepts strings, and reading gives you strings — no casts.
Generic methods
Put the type parameter in angle brackets after the method name:
T First<T>(T[] items) => items[0];
void PrintTwice<T>(T value)
{
Console.WriteLine(value);
Console.WriteLine(value);
}
string s = First(new[] { "a", "b" }); // T inferred as string
int n = First<int>(new[] { 1, 2 }); // or given explicitlyThe compiler usually infers T from the arguments.
Generic classes
class Box<T>
{
public T Value { get; }
public Box(T value) => Value = value;
public Box<TResult> Map<TResult>(Func<T, TResult> transform) => new(transform(Value));
}
var number = new Box<int>(21);
var doubled = number.Map(x => x * 2); // Box<int>
var text = number.Map(x => $"#{x}"); // Box<string>Use several parameters when needed — Dictionary<TKey, TValue>, Pair<TFirst, TSecond>. By convention, type parameters start with T.
Constraints
Inside a generic method, T could be anything, so you can only use what every type has (the members of object). To use more, add a constraint:
T Largest<T>(T[] items) where T : IComparable<T>
{
T best = items[0];
foreach (T item in items)
if (item.CompareTo(best) > 0) best = item; // CompareTo is available thanks to the constraint
return best;
}
Largest(new[] { 3, 9, 2 }); // 9
Largest(new[] { "pear", "zebra" }); // "zebra"| Constraint | Meaning |
|---|---|
where T : SomeInterface | T must implement the interface |
where T : SomeBaseClass | T must derive from the class |
where T : class | T must be a reference type |
where T : struct | T must be a value type |
where T : new() | T must have a parameterless constructor |
where T : notnull | T must be a non-nullable type |
default(T)
What's an "empty" value for an unknown type? default — 0 for numbers, false for bool, null for reference types:
T FirstOrDefault<T>(List<T> items) => items.Count > 0 ? items[0] : default!;Why generics matter
- Type safety — mistakes are caught at compile time, not with a crash.
- No casts — code is cleaner.
- Performance —
List<int>stores real ints; a list ofobjectwould box every number into a separate heap object.
Tip
Most of the time you'll use generic types from the BCL — collections, Task<T>, LINQ. Writing your own generic classes comes up mostly when building reusable infrastructure like repositories or result types.
Try it
C# runtime not loaded
Key takeaways
- Generics let one class or method work with many types, safely.
- Type parameters (
T) are filled in by type arguments (List<int>) or inferred by the compiler. - Constraints (
where T : ...) unlock members you can use onT. - Prefer generics over
object: type safety, no casts, no boxing.
Exercise
Implement the generic method Largest (returns the biggest item of any comparable type), Repeat (a list containing the item the given number of times) and Pair.Swap (returns a new Pair with First and Second exchanged — note the type parameters swap too).
C# runtime not loaded
Hint 1
Largest: keep a best-so-far variable and compare with item.CompareTo(best) > 0 — the constraint is what makes CompareTo available.
Hint 2
Repeat: create a List<T>, Add the item in a loop, return the list.
Hint 3
Swap: new Pair<TSecond, TFirst>(Second, First).
Show solution
Console.WriteLine(Largest(new[] { 3, 9, 2 }));
Console.WriteLine(string.Join(", ", Repeat("ab", 3)));
Console.WriteLine(new Pair<string, int>("age", 36).Swap());
T Largest<T>(T[] items) where T : IComparable<T>
{
T best = items[0];
foreach (var item in items)
{
if (item.CompareTo(best) > 0) best = item;
}
return best;
}
List<T> Repeat<T>(T item, int times)
{
var list = new List<T>();
for (int i = 0; i < times; i++) list.Add(item);
return list;
}
class Pair<TFirst, TSecond>
{
public TFirst First { get; }
public TSecond Second { get; }
public Pair(TFirst first, TSecond second)
{
First = first;
Second = second;
}
public Pair<TSecond, TFirst> Swap() => new Pair<TSecond, TFirst>(Second, First);
public override string ToString() => $"({First}, {Second})";
}Quiz
4 questions · 3 correct to pass1.What does the T in `List<T>` represent?
2.Why is `List<int>` better than a list of object?
3.What does `where T : IComparable<T>` do?
4.When calling `Largest(new[] { 3, 9, 2 })`, why don't you need to write `Largest<int>(...)`?
To complete this lesson
- ○ Solve the exercise
- ○ Pass the quiz (3/4 correct)