Skip to content

Intermediate · Collections, generics and LINQ · topic 23 of 43 · 30 min

Generics: reusable, type-safe code

Explanation

Generics let you write a class or method once with a placeholder type T, and use it with any type while keeping full type checking. List<T> and Dictionary<TKey,TValue> are generic. Constraints like where T : IComparable<T> tell the compiler what T can do, so you can call those members safely.

The problem generics solve

Before generics (C# 2.0), collections stored everything as object. You could put an int and a string into the same ArrayList, and you had to cast every item back, which could fail at run time. Value types were boxed, which was slow. Generics fixed this: List<int> holds only ints, needs no casts and does not box.

csharp
var old = new System.Collections.ArrayList();
old.Add(10);
old.Add("ten");               // allowed, sadly
int first = (int)old[0]!;     // cast needed
// int second = (int)old[1]!; // compiles, but InvalidCastException at run time

var modern = new List<int> { 10 };
// modern.Add("ten");         // compile error: caught early
int a = modern[0];            // no cast
Generics move errors from run time to compile time.
Think of it like this: A generic class is like a tiffin box design that comes in any size. The design (lid, clips, compartments) is written once. When you order one, you say "for rice" or "for chapatis" and get a box fitted for exactly that, so you cannot accidentally pour dal into the chapati box.

Generic methods

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

int x = 1, y = 2;
Swap(ref x, ref y);          // T is inferred as int
string p = "Pune", q = "Goa";
Swap(ref p, ref q);          // T is string
Console.WriteLine($"{x} {y} {p} {q}");   // 2 1 Goa Pune
<T> after the name declares a type parameter. The compiler usually infers T from the arguments.

Generic classes

csharp
Result<decimal> fare = Result<decimal>.Ok(1250m);
Result<string> pnr = Result<string>.Fail("PNR not found");
Console.WriteLine(fare.Success ? $"Fare {fare.Value}" : fare.Error);
Console.WriteLine(pnr.Success ? pnr.Value : pnr.Error);

public class Result<T>
{
    public bool Success { get; }
    public T? Value { get; }
    public string? Error { get; }

    private Result(bool success, T? value, string? error)
    {
        Success = success; Value = value; Error = error;
    }

    public static Result<T> Ok(T value) => new(true, value, null);
    public static Result<T> Fail(string error) => new(false, default, error);
}
One Result<T> works for any kind of value. default gives the default value of T (0, false or null).

Constraints: telling the compiler what T can do

Inside a generic method, the compiler only knows that T is "some type". You cannot use > on it, call a method on it, or create it with new, unless you add a constraint with where.

csharp
static T Largest<T>(IEnumerable<T> items) where T : IComparable<T>
{
    T best = items.First();
    foreach (T item in items)
        if (item.CompareTo(best) > 0) best = item;
    return best;
}

Console.WriteLine(Largest(new[] { 4, 19, 7 }));               // 19
Console.WriteLine(Largest(new[] { "mango", "apple", "kiwi" })); // mango
Without the constraint, item.CompareTo would not compile, because not every type has it.
ConstraintMeaning
where T : classT must be a reference type
where T : structT must be a non-nullable value type
where T : notnullT must not be a nullable type
where T : new()T must have a public parameterless constructor, so you can write new T()
where T : SomeBaseClassT must be that class or derive from it
where T : ISomeInterfaceT must implement that interface

Generic interfaces: a repository

csharp
public interface IRepository<T> where T : class
{
    void Add(T item);
    T? FindById(int id);
    IReadOnlyList<T> All();
}
One interface describes storage for customers, orders or products. You will see this shape in many company codebases.

Common mistakes and fixes

  • Using operators like > or + on T: error CS0019. Use IComparable<T> (or, in modern .NET, generic math interfaces such as INumber<T>, which are more advanced).
  • Writing new T() without where T : new(): error. Add the constraint.
  • Making everything generic: if a method only ever works with Order, use Order. Generics are for code that truly works with many types.
  • Returning null from a method returning T without a constraint: use default, or constrain T to class and return T?.
Tip: Type parameter names start with T: T for one, or descriptive names like TKey and TValue when there are several.

Need help?

Stuck on a word or an idea? Search this course, or every course, and jump straight to the explanation or a worked question.

Try:

Watch the lesson

Generics: reusable, type-safe code · 1/7

Generics

In this lesson you will write code that works with any type, safely: generics.

Prefer a video by a teacher? Find free videos on “C# generics tutorial generic methods classes constraints where T” on YouTube ↗

In real life: A paged API response for a product catalogue

Shopping apps show products page by page. The API returns the same wrapper for products, reviews or orders: the items on this page, the page number and the total count. One generic class serves every list in the app.

csharp
var page = Paginate(new[] { "Kurta", "Saree", "Dupatta", "Lehenga", "Sherwani" }, pageNo: 2, size: 2);
Console.WriteLine($"Page {page.PageNo} of {page.TotalPages}: {string.Join(", ", page.Items)}");

static PagedResult<T> Paginate<T>(IReadOnlyList<T> all, int pageNo, int size)
{
    var items = new List<T>();
    for (int i = (pageNo - 1) * size; i < Math.Min(pageNo * size, all.Count); i++)
        items.Add(all[i]);
    return new PagedResult<T>(items, pageNo, (all.Count + size - 1) / size);
}

public record PagedResult<T>(IReadOnlyList<T> Items, int PageNo, int TotalPages);
Shows Page 2 of 3: Dupatta, Lehenga. Records can be generic too.

Practice (0/4 done)

Task 1 · Question

What is the main benefit of List<int> over the old ArrayList?

Task 2 · Question

static T First<T>(T[] arr) => arr[0]; What is the type of First(new[] { 2.5, 3.5 })?

Task 3 · Question

Which constraint lets you write new T() inside a generic method?

Task 4 · Practice

Write a generic class Cache<TKey, TValue> (TKey must be notnull) that wraps a Dictionary and has: Set(key, value), TryGet(key, out TValue value) and a Count property. Use it once with <string, decimal> for product prices and once with <int, string> for roll numbers to names.

Quick quiz

Question 1

1.In List<T>, what is T?

Question 2

2.static bool Bigger<T>(T a, T b) => a > b; Why does this not compile?

Question 3

3.What does default give for T = int and for T = string?

Question 4

4.Which is a generic type in .NET?

Question 5

5.where T : class means...

Read more (free)

Open a question to see the answer and why. The first ones are this topic's own quiz, so try the quiz above before you peek.

In List<T>, what is T?

Answer: A type parameter (placeholder for a type)

T is filled in with a real type when you use the list.

static bool Bigger<T>(T a, T b) => a > b; Why does this not compile?

Answer: The compiler does not know T supports >

Add a constraint such as IComparable<T> and use CompareTo.

What does default give for T = int and for T = string?

Answer: 0 and null

default is 0 for int and null for reference types.

Which is a generic type in .NET?

Answer: Dictionary<TKey,TValue>

Dictionary has two type parameters.

where T : class means...

Answer: T must be a reference type

class here means any reference type.

In which Go version did generics arrive?Related

Answer: 1.18

Go 1.18 (March 2022) added type parameters.

From Generics: type parameters and constraints

Which built-in type is generic?Related

Answer: Promise<T>

Promise<T> carries the type of its eventual value.

From Generics

What do generics mainly give you?Related

Answer: Type safety at compile time without casts

Errors move from run time to compile time.

From Generics

Which utility type makes every property optional, for an "edit profile" form where any field may change?Related

Answer: Partial

Partial<T> adds ? to every property of T.

From Generics and utility types

Pass the quiz (60% or more) or mark this topic as done when you are ready.

Your progress is saved on this device only.

Next steps

  1. 2 more topics in “Collections, generics and LINQ” before its exam: LINQ basics: query data in a few lines, LINQ in depth: grouping, joins, deferred execution, IQueryable.
  2. Module 6 exam: Collections, generics and LINQ · 14 questions · 25 min, without help. 4 topics of this module still to finish first.
  3. Then the next module, Exceptions, delegates, events and async. 43 topics left in the course before the final exam.

← Previous: Collections: List, Dictionary, HashSet, Queue and Stack