.NET Academy

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 wrong

Generics 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 explicitly

The 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"
ConstraintMeaning
where T : SomeInterfaceT must implement the interface
where T : SomeBaseClassT must derive from the class
where T : classT must be a reference type
where T : structT must be a value type
where T : new()T must have a parameterless constructor
where T : notnullT 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 of object would 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

Program.cs
Loading editor…

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 on T.
  • 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).

Program.cs
Loading editor…

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 pass
  1. 1.What does the T in `List<T>` represent?

  2. 2.Why is `List<int>` better than a list of object?

  3. 3.What does `where T : IComparable<T>` do?

  4. 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)