Skip to content

~ / blog / csharp-generics-explained

C# Generics Explained From Zero

updated 2 Jul 2026 C# 11 min read

This is the chapter I wish I had when generics first confused me. No jargon up front. We build the idea one small step at a time, using a plain Book class, and by the end you will understand generics well enough to answer any standard interview question on them.

The problem generics solve

Say you write a class whose only job is to hold one number:

class NumberBox
{
    public int Item;
}

It works, but it only holds an int. Tomorrow you need a box for text, so you write another class:

class TextBox
{
    public string Item;
}

Notice it is the exact same class. Only the type changed. Next you will need a box for Book, then Product, then DateTime. You would copy and paste the same class forever, changing one word each time.

That copy and paste is the problem generics remove.

The generic version

Instead of writing the type into the class, you leave a blank:

class Box<T>
{
    public T Item;
}

T is not a real type. It is a placeholder. A blank space that means “some type, decided later.”

The blank gets filled the moment you use the class:

var box1 = new Box<int>();      // T becomes int
box1.Item = 5;                  // works

var box2 = new Box<string>();   // T becomes string
box2.Item = "hello";            // works

box1.Item = "hello";            // ERROR, box1 only takes int

One class, any type, and the compiler still catches mistakes like that last line.

You already use this every day. List<int>, List<string>, Dictionary<TKey, TValue>, Task<T>. Microsoft wrote each once and they work for every type because of the blank.

A generic class is just a normal class with a blank

Here is a slightly fuller version with a constructor and a method. Every piece is something you already know from ordinary classes. The only new thing is T sitting where a concrete type used to be.

public class Box<T>
{
    public T Item { get; set; }

    public Box(T item)      // constructor, assigns like any other
    {
        Item = item;
    }

    public T GetItem()      // returns T, just like a method returning a float
    {
        return Item;
    }
}

Used like this, with a plain Book class that has Title, Author and Price:

var bookBox = new Box<Book>(new Book("Test", "Author", 123f));
Console.WriteLine(bookBox.GetItem().Title);   // no casting needed

var wordBox = new Box<string>("Hello");
Console.WriteLine(wordBox.GetItem());          // same class, different type

Why not just use object?

This is the question worth asking, because object looks like it would do the same job. object is the base type of everything in C#, so it can hold any type:

public class Box
{
    public object Item { get; set; }
    public Box(object item) { Item = item; }
    public object GetItem() { return Item; }
}

It stores a Book fine. But getting it back out breaks:

Console.WriteLine(box.GetItem().Title);   // COMPILE ERROR

GetItem() returns object, and object has no .Title. The compiler forgot it was a Book. To use it you are forced to cast:

Book b = (Book)box.GetItem();
Console.WriteLine(b.Title);

Three problems come with that, and together they are the whole argument for generics.

1. You lose the type and cast everywhere. Every time you pull something out, you cast. Clutter. With Box<Book>, GetItem() already returns Book. No cast, ever.

2. A wrong cast compiles fine and crashes at runtime.

var box = new Box(new Book("Test", "Author", 123f));
string s = (string)box.GetItem();   // compiles, then crashes at runtime

You told the compiler “trust me, it is a string.” It believed you. The bug survived compilation and reached your users. With generics this is impossible, the compiler catches it before the program builds.

3. Boxing. Putting an int into an object wraps it in a heap allocation, and pulling it out unwraps it. In a loop over millions of items that overhead is real. Box<int> stores the int directly.

The summary: object throws away the type and hands the risk to you at runtime. Generics keep the type and let the compiler enforce it before the program runs. Same flexibility, but safe and fast instead of loose and slow. This is exactly why List<T> replaced the old object based ArrayList and nobody went back.

Constraints

Generics are safe because the compiler assumes almost nothing about T. But that safety has a cost. Because T could be anything, you can barely do anything to it. Try to print a price:

public class Box<T>
{
    public T Item { get; set; }
    public void PrintPrice() { Console.WriteLine(Item.Price); }  // COMPILE ERROR
}

The compiler refuses with 'T' does not contain a definition for 'Price'. Its reasoning: someone might make a Box<string>, and a string has no .Price. The method must be valid for every possible T, so it is blocked for everyone.

A constraint is a promise to the compiler about what T will be. You make it with where:

public class Box<T> where T : Book
{
    public T Item { get; set; }
    public Box(T item) { Item = item; }

    public void PrintPrice()
    {
        Console.WriteLine(Item.Price);   // NOW it compiles
    }
}

Now the compiler knows every T is at least a Book, and every Book has .Price, so the line is safe.

The promise is real, and it costs you flexibility:

var bookBox = new Box<Book>(...);        // fine, Book satisfies the constraint
var wordBox = new Box<string>("Hello");  // NOW A COMPILE ERROR

The moment you add the constraint, Box<string> stops being allowed. That is the deal constraints make. You narrow what T can be, and in exchange you can do more with it.

The mental model:

  • No constraint means T can be anything, so you can do almost nothing to it.
  • where T : Book means T must be a Book, so you can do everything a Book can do.

You are buying abilities with restrictions.

The five kinds of constraint

You read them all the same way, “T must be…”:

where T : Product       // must be Product or a subclass
where T : IComparable    // must implement this interface
where T : class          // must be a reference type
where T : struct         // must be a value type (int, bool, and so on)
where T : new()          // must have a parameterless constructor, so you can do new T()

You can stack them. This is a real example from the standard C# course material:

public class Utilities<T> where T : IComparable, new()
{
    public T Max(T a, T b)
    {
        return a.CompareTo(b) > 0 ? a : b;   // needs IComparable
    }

    public void DoSomething()
    {
        var obj = new T();                   // needs new()
    }
}

IComparable lets Max call CompareTo, and new() lets DoSomething create a fresh T.

One note: you cannot constrain to a sealed type like string

A common question is “how do I make a generic that only allows string?” You cannot write where T : string. The compiler rejects it, because string is sealed and nothing can inherit from it, so the blank would only ever be exactly one type. At that point generics are pointless. If it is always string, do not use generics. Just write string directly and delete the <T>.

Generic methods

A single method can be generic even inside a normal, non generic class. The <T> sits on the method:

public T Max<T>(T a, T b) where T : IComparable
{
    return a.CompareTo(b) > 0 ? a : b;
}

This is why FirstOrDefault() and friends in LINQ return the right type without you casting. You will meet generic methods constantly once you reach LINQ.

The same idea inside Google, Meta and Netflix

Generics are not a C# quirk. The idea of writing the logic once, letting the caller supply the type, and keeping the compiler checking everything exists in every serious language, and the biggest codebases on earth lean on it constantly. Only the spelling changes.

Microsoft / Stack Overflow (.NET). The clearest example is one you may already have used: Dapper, the micro ORM built by Stack Overflow’s engineers to run stackoverflow.com. Its whole API is one generic method:

var books = connection.Query<Book>("SELECT * FROM Books");

Query<T> is exactly our Box<T> thinking at production scale: one method, any table, no casting, and the compiler knows every row is a Book. The same pattern runs through ASP.NET Core itself, from ILogger<HomeController> and IOptions<JwtSettings> to EF Core’s DbSet<TEntity>. This is why a generic repository like Repository<T> feels so natural in .NET backends.

Google (Java, Go, C++). Google’s Java code is built on generic collections. Its Guava library gives you ImmutableList<E> and Cache<K, V>, the same “blank filled by the caller” as List<T>. On Android, LiveData<T> holds any screen state. And Go famously lived without generics for a decade. Teams at Google copy-pasted or fell back to interface{}, which is their version of object, until the pain got so real that Go 1.18 added generics in 2022. That whole debate was our “why not just use object?” section playing out in public.

Meta (TypeScript, Hack). If you have written React with TypeScript, you have used generics: useState<Book[]>([]) is a generic function call, and Meta’s own Hack language uses vec<T> and Map<Tk, Tv> across Facebook’s codebase. Same blank, same compile time safety.

Netflix (Java). Netflix built RxJava, where every data stream is an Observable<T>, one streaming pipeline that works for any event type, checked at compile time.

The takeaway for interviews and for reading unfamiliar code: Query<T>, ImmutableList<E>, useState<T>, Observable<T>. Different companies, different languages, one concept. Learn it once in C# and you recognise it everywhere.

Summary

Generics give you one class or method that works with many types, while keeping full type safety.

  • T is a blank filled in by the caller at the point of use.
  • Generics beat object because they keep the type, remove casting, catch mistakes at compile time instead of runtime, and avoid boxing.
  • A constraint (where T : ...) is a promise about what T can be. It narrows the allowed types in exchange for letting you do more with T.
  • The five constraints are base class, interface, class, struct, and new().
  • Methods can be generic too, not just classes.

Interview questions and answers

1. What is a generic in C#?

A way to write a class or method once and have it work with any type, while keeping compile time type safety. You use a type parameter such as T as a placeholder, and the actual type is supplied by the caller when the class or method is used. Examples: List<T>, Dictionary<TKey, TValue>, Task<T>.

2. Why use generics instead of object?

Three reasons, strongest first:

  • Type safety at compile time. A wrong cast on an object compiles and then crashes at runtime. With generics the compiler catches it before the program builds.
  • No casting. The type comes back correctly on its own, so the code is cleaner.
  • No boxing. Value types are stored directly instead of being wrapped in a heap allocation, which matters for performance in large loops.

3. What does T represent?

Any type the caller chooses at the point of use. Repository<Book> makes T become Book, Repository<int> makes T become int. One class, the caller decides the type.

4. Why does this fail to compile, and how do you fix it?

public class Box<T>
{
    public T Item { get; set; }
    public void Show() { Console.WriteLine(Item.Price); }
}

It fails because the compiler does not know T has a .Price. T could be any type, including one with no price. The fix is a constraint, for example where T : Product, which promises every T is a Product and therefore has a .Price.

5. What is a constraint and what does it cost you?

A constraint is a promise to the compiler about what T can be, written with where. It lets you do more with T, but it narrows the set of types allowed. Adding where T : Book means you can now call Book members on T, but Box<string> is no longer permitted.

6. Name the constraint types.

Base class (where T : SomeClass), interface (where T : IComparable), reference type (where T : class), value type (where T : struct), and parameterless constructor (where T : new()). They can be combined.

7. Can you constrain a generic to only allow string?

No. where T : string is not allowed because string is sealed, so nothing can inherit from it and T could only ever be exactly string. If a type is always string, do not use generics at all.

8. What is a generic method?

A method that declares its own type parameter, so it can be generic even inside a non generic class, for example public T Max<T>(T a, T b). This is how many LINQ methods return the correct type without casting.

9. Why did generics replace ArrayList?

ArrayList stored everything as object, which meant casting on every read and casting bugs that only appeared at runtime. List<T> gives the same flexibility with compile time safety and no boxing, so it replaced ArrayList for almost all use.

10. What is the difference between a generic class and a generic method?

A generic class puts the type parameter on the class, so every member can use it: class Box<T>. A generic method puts the type parameter on the method itself, so only that method is generic: T Max<T>(T a, T b). A normal class can contain generic methods, and a generic class can contain non generic methods.

# tags csharpgenericsdotnetinterview-questions
$ share linkedin x / twitter

Tayyab Shahid, Senior Software Engineer in London

Tayyab Shahid

Senior Software Engineer & .NET Consultant · London, UK

about github linkedin

§ related posts

Building something like this?

I take on freelance .NET projects, consulting, and senior roles.

get in touch

← cd ~/blog

CV