C# · Intermediate

Generics

Generics let you write code once and use it with any type — while keeping full type safety. List<T> is generic; now you will write your own.

Intermediate 6 min Lesson 18 of 26Examples target C# 13 on .NET 9 unless stated otherwise.
0/26 in C#
On this page · 6 min · Intermediate

In simple words#

A lunch box that can hold any food type you choose — List<int> holds numbers, List<string> holds text. One design, any type, still type-safe.

Generic
Code written once for many types.
<T>
A placeholder for the type you choose.
Type safety
The compiler stops you mixing wrong types.

Learning objectives#

  • Read and write generic types like Box<T>
  • Write generic methods
  • Restrict type parameters with where constraints
  • Explain why generics beat object

Explanation#

A type parameter (usually T) is a placeholder for a real type. In List<string>, T becomes string, so the list only accepts strings and gives you strings back — no casting.

A generic class: class Box<T> { public T Value; }. A generic method: static T Max<T>(T a, T b). The compiler often infers T from the arguments, so you write Max(3, 9).

Constraints limit which types T may be: where T : class (reference types), where T : new() (has a parameterless constructor), where T : IComparable<T> (can be compared), where T : IEntity (implements your interface).

Before generics, collections stored object, which needed casts and allowed mistakes at runtime. Generics catch those mistakes at compile time and avoid boxing for value types.

A generic Repository<T> is a classic real-world use: one class handles Product, Customer and Order.

Syntax#

C# Illustrative
class Box<T>
{
    public T Value { get; set; }
}


static T First<T>(List<T> items) => items[0];


class Repository<T> where T : IEntity { }

Code example: Boxes, Max and a generic repository#

C#Program.cs Illustrative
var number = new Box<int>(42);
var word = new Box<string>("hello");
Console.WriteLine(number.Describe());
Console.WriteLine(word.Describe());


Console.WriteLine(Max(3, 9));
Console.WriteLine(Max("apple", "mango"));


var repo = new Repository<Product>();
repo.Add(new Product(1, "Pen"));
repo.Add(new Product(2, "Book"));
Console.WriteLine(repo.GetById(2)?.Name);
Console.WriteLine(repo.GetById(9)?.Name ?? "Not found");


static T Max<T>(T a, T b) where T : IComparable<T> =>
    a.CompareTo(b) >= 0 ? a : b;


class Box<T>(T value)
{
    public T Value => value;
    public string Describe() => 
quot;Box<
{typeof(T).Name}> holding {value}";
} interface IEntity { int Id { get; } } class Product(int id, string name) : IEntity { public int Id => id; public string Name => name; } class Repository<T> where T : IEntity { private readonly List<T> _items = []; public void Add(T item) => _items.Add(item); public T? GetById(int id) => _items.FirstOrDefault(item => item.Id == id); }
Expected output
Box<Int32> holding 42
Box<String> holding hello
9
mango
Book
Not found

This example is illustrative. Copy or download it and run it in your own environment; the expected output is shown above.

Step-by-step explanation#

  1. Box<int> and Box<string> are two types made from one definition. typeof(T).Name shows the real type.
  2. Max<T> works for any comparable type; the compiler infers T as int, then string.
  3. where T : IComparable<T> is what allows a.CompareTo(b) inside the method.
  4. Repository<T> requires T : IEntity, so it can safely read item.Id.
  5. GetById returns T? so “not found” is a normal, null result.

Real-world use case#

ASP.NET Core APIs often return a generic wrapper such as ApiResponse<T> or PagedResult<T>, and use generic repositories or EF Core’s DbSet<T> so the same code serves every entity.

Common mistakes#

Using object instead of generics

A List of object forces casts and lets a string sneak into a list of numbers. Use List<T>.

Calling members T does not promise

Inside Max<T> without a constraint, a.CompareTo(b) does not compile. Add where T : IComparable<T>.

Making everything generic

Generics add complexity. Introduce them when you really have the same logic for several types.

Best practices#

  • Name type parameters T, or TKey/TValue when there are several
  • Add the narrowest constraint that makes your code compile
  • Prefer built-in generic collections
  • Return T? from lookups that can fail

Remember this#

T is a placeholder filled in when you use the type.

where T : X = “T must be an X”.

Generics = reuse + type safety, no casts.

Practice exercise#

Your task: Write a generic method Swap<T>(ref T a, ref T b) that swaps two values, and test it with ints and strings.

Need a hint?
Use a temporary variable of type T. Call it with Swap(ref x, ref y).
Show solution
C# Illustrative
int x = 1, y = 2;
Swap(ref x, ref y);
Console.WriteLine(
quot;
{x} {y}"); // 2 1
string a = "left", b = "right"; Swap(ref a, ref b); Console.WriteLine(
quot;
{a} {b}"); // right left
static void Swap<T>(ref T a, ref T b) { T temp = a; a = b; b = temp; }

Mini quiz#

Generics quizMultiple choice Question 1 of 3

What does where T : IComparable<T> mean?

Score so far: 0/0

Want more? Practise all C# MCQs · C# interview questions

My notes#

Saved in this browser.

Progress is saved in this browser. Sign in to sync.