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.
On this page · 6 min · Intermediate
On this page
- In simple words
- Learning objectives
- Explanation
- Syntax
- Code example
- Step by step
- Real-world use
- Common mistakes
- Best practices
- Remember this
- Practice exercise
- Mini quiz
- Related topics
- My notes
- Reading time
- 6 min
- Difficulty
- Intermediate
- Updated
- Oct 28, 2026
Related tutorials
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#
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#
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);
}
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#
- Box<int> and Box<string> are two types made from one definition. typeof(T).Name shows the real type.
- Max<T> works for any comparable type; the compiler infers T as int, then string.
- where T : IComparable<T> is what allows a.CompareTo(b) inside the method.
- Repository<T> requires T : IEntity, so it can safely read item.Id.
- 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#
A List of object forces casts and lets a string sneak into a list of numbers. Use List<T>.
Inside Max<T> without a constraint, a.CompareTo(b) does not compile. Add where T : IComparable<T>.
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?
Show solution
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#
What does where T : IComparable<T> mean?
Want more? Practise all C# MCQs · C# interview questions
My notes#
Saved in this browser.