AlgoMaster Logo

Generics Basics

Last Updated: May 22, 2026

High Priority
11 min read

Generics let you write a class, method, or interface that works with any type without giving up compile-time type safety. They're how List<T>, Dictionary<TKey, TValue>, and most of the BCL stay reusable and fast at the same time. This chapter explains why generics exist, what the <T> notation actually means, and the vocabulary used throughout the rest of the section. The siblings drill into each shape, this one stays at the survey level on purpose.

The Problem Generics Solve

Before generics arrived in C# 2.0, there were two ways to write a container that could hold anything. You could write one container per element type (a StringList, an IntList, a ProductList, each a near-identical copy), or you could write a single container that stored every item as object and let the caller cast on the way out. Both options were unpleasant in different ways.

The single-container approach was the more common pick, because the copy-paste version was a maintenance nightmare. The result was types like ArrayList, from the original System.Collections namespace, where every item is stored as object:

That code runs fine. The trouble starts when someone, six months later, decides the cart should only hold product names and writes this:

The compiler is happy. The program throws InvalidCastException at runtime on the second iteration, because 29.99 is a double, not a string. Nothing in the code declares "this list should only hold strings," so the mistake doesn't get caught until the program runs.

There are three concrete problems here, and they show up together:

The first is no compile-time type safety. The ArrayList accepts anything, because its Add method takes an object parameter, and every type in C# inherits from object. The author's intent (only strings) isn't expressed in the type, so the compiler can't help.

The second is runtime casts everywhere. To do anything useful with an item, you have to cast it back to its real type. Every cast is a check the runtime has to perform, and every cast is a place a future change can introduce a crash.

The third is boxing for value types. Value types like int, double, and decimal are stored directly, but the moment you put one into an object-shaped slot, the runtime allocates a small wrapper object on the heap and copies the value into it. That's called boxing. Reading the value back out unwraps it, which is called unboxing. Both have a cost in time and memory.

Here's what boxing looks like in memory. The left side is a raw int variable on the stack. The right side is the same int after it gets stored in an object slot, copied onto the heap inside a wrapper.

The raw int is four bytes on the stack. The boxed version is a reference on the stack pointing to a heap object that holds the four bytes plus an object header (type info and a sync block, typically sixteen bytes of overhead on 64-bit). For a single value the overhead is invisible. For a million of them it's the difference between four megabytes and twenty.

Storing a million int values in an ArrayList allocates a million tiny heap objects, plus the array of references pointing at them. The same data in a List<int> is one contiguous array of four-byte integers. The generic version uses roughly a fifth of the memory and iterates faster because the CPU cache benefits from contiguous data.

The Generic Fix in One Picture

Generics solve all three problems at once by letting a type or method declare a placeholder for the element type, and letting the compiler fill the placeholder in when the type is used. That placeholder is the famous <T>.

Here's the same cart, rewritten with List<T>:

Three things changed. The compiler now rejects the double at the Add call, because List<string>.Add takes a string parameter, not an object. No cast is needed in the foreach, because the loop variable is already typed as string. And if the list held int values instead, they'd be stored as raw integers in the underlying array, with no boxing.

Same problem, all three downsides removed. That's the whole pitch for generics. The rest of this chapter unpacks how the <T> notation works and what each piece is called, because the vocabulary will come up in every chapter that follows.

The contrast between the two storage models is worth seeing as a memory layout. An ArrayList of three int values is an array of references pointing at three heap-allocated boxes. A List<int> of the same three values is one array of raw integers, no boxes, no extra references.

The right-hand layout is what makes generic collections fast for value types. The CPU walks contiguous bytes, and every read is one memory access instead of two.

What <T> Actually Means

T is a type parameter. It's a name that stands in for a type, the same way a method parameter is a name that stands in for a value. You write T when you define a generic type or method, and the actual type gets supplied later when someone uses it.

A method parameter goes from declaration to call like this:

A type parameter goes from declaration to use like this:

The vocabulary line everyone needs to learn:

  • Type parameter: the placeholder name in the definition. The T in class Box<T>.
  • Type argument: the concrete type supplied at the use site. The string in Box<string>.

By convention, type parameter names start with T. A type that takes one parameter uses T. A type that takes a key and a value uses TKey and TValue. A type representing a result uses TResult. The pattern is "uppercase T followed by a descriptive PascalCase name." You'll see this all over the BCL: Dictionary<TKey, TValue>, Func<TResult>, IComparer<T>, PriorityQueue<TElement, TPriority>. The names are documentation as much as identifiers, so picking good ones matters when you start writing your own generic types in the _Generic Classes_ lesson.

A type parameter is not a variable, not an object, not a base class. It is a name. At compile time, the compiler treats every occurrence of T inside the definition as "whatever type the caller will supply." At runtime, the CLR substitutes the actual type. There's no inheritance involved, no casting, no boxing. A List<int> and a List<string> are two different, fully specialised types, and the runtime treats them as such.

The Three Shapes: Classes, Methods, Interfaces

Generics apply to three different language constructs in C#. The detailed teaching for each one is in the _Generic Classes_, _Generic Methods_, and _Generic Interfaces_ lessons, but this section walks through the shape of each, just enough to recognise each form.

Generic Classes

A generic class is a class that takes one or more type parameters in its declaration. Inside the class, the parameters can be used anywhere a real type could be: field types, property types, parameter types, return types, local variable types. The classic example is a container.

One class definition. Two specialised versions in use: Wishlist<string> and Wishlist<int>. The compiler enforces that you can only add strings to the first and ints to the second. The runtime stores the integers as raw int values in the backing array, no boxing. The _Generic Classes_ lesson covers how to declare these properly, including multiple type parameters, default values, and the pitfalls that come with backing fields.

Generic Methods

A generic method is a method that takes its own type parameters, separately from the class it lives in. The angle brackets go after the method name, before the parameter list. The method can be inside a normal class, a generic class, or a static utility class. The most common case is a utility method that operates on any type.

The calls to Swap don't write Swap<string> or Swap<int> explicitly. The compiler figures the type out from the argument types. That's called type inference, and it's why most generic methods look like ordinary method calls at the call site. You can still write the type explicitly (CartUtils.Swap<string>(ref a, ref b)) when the compiler can't infer it or when the code is clearer that way. The _Generic Methods_ lesson covers how to declare them, how type inference works, and when you have to spell out the type argument by hand.

Generic Interfaces

A generic interface is an interface that takes type parameters. The BCL is full of them: IEnumerable<T>, IComparer<T>, IEqualityComparer<T>, IList<T>, IReadOnlyDictionary<TKey, TValue>. A class that implements a generic interface picks a concrete type for each parameter at the implementation site.

PriceComparer implements IComparer<decimal>, picking decimal as the concrete type for the interface's type parameter. The Sort method on List<decimal> accepts an IComparer<decimal> and uses it to order items. The same IComparer<T> interface gets reused across every type that needs custom ordering. The _Generic Interfaces_ lesson covers how to define and implement them, including the difference between implementing for one specific type and leaving the interface itself open.

The three shapes share one big idea: the type parameter is part of the definition, and the concrete type gets plugged in at the use site. The differences are syntactic and scoping. Class parameters live for the lifetime of the instance, method parameters live for the lifetime of the call, interface parameters live wherever the interface is implemented.

Generic Type Definitions, Constructed Types, and Open vs Closed

Two more terms come up constantly in C# documentation, and they trip people up the first time they meet them.

A generic type definition is the form with the parameters still unbound. List<T> is the generic type definition for the list type. It's a template, not a usable type by itself. You can refer to it in reflection code (typeof(List<>) uses the special <> syntax to mean "the open definition"), but you can't create an instance of it.

A constructed generic type is the form with concrete types supplied. List<string> is a constructed type. It is a real, usable type. You can create instances of it, store references to it, and the runtime treats it as a distinct type from List<int>.

When every type parameter has been supplied, the constructed type is called closed. List<string> is closed. Dictionary<string, decimal> is closed. The compiler can generate code for closed types because every type is known.

When at least one type parameter is still a parameter (rather than a concrete type), the type is open. This case mostly comes up inside generic code that's parameterised on yet another type. Inside a class Repository<T>, the type List<T> is open because T isn't known until Repository<T> itself is constructed. Once you write new Repository<Product>(), every T becomes Product, the inner List<T> becomes List<Product>, and everything is closed.

TermMeaningExample
Generic type definitionThe template form with unbound parametersList<T> (in declaration)
Type parameterThe placeholder name in the definitionT in class List<T>
Type argumentThe concrete type supplied at the use sitestring in List<string>
Constructed typeA type with type arguments plugged inList<string>
Open typeA constructed type that still has unbound parametersList<T> inside Repository<T>
Closed typeA constructed type with every parameter bound to a concrete typeList<string>, Dictionary<int, Product>

The vocabulary matters because the rest of this section, and most of the official Microsoft documentation, uses these terms without re-introducing them. When the _Generic Constraints_ lesson talks about "constraints on the type parameter," it's talking about the T in the definition. When the runtime error messages mention "the open generic type List<>", they mean the unbound template.

Why Generics Are Worth Learning

Five payoffs come out of generics, and you've already seen them in the examples above. Stating them in one place makes them easier to remember when picking between approaches later.

Compile-time type safety. The compiler knows what types belong in a generic container or method, so type mismatches become errors before the program runs. The earlier the error, the cheaper it is to fix. A CS1503 at compile time is free. A InvalidCastException in production is expensive.

No boxing for value types. Generic types store int, double, decimal, and other value types in their actual form, without allocating heap wrappers. For collections of any meaningful size, this is the difference between cache-friendly contiguous data and a maze of pointer chasing.

Code reuse without copy-paste. One class Stack<T> definition covers Stack<int>, Stack<string>, Stack<Order>, and any other type you care to plug in. Before generics, you either copied the class for every element type or accepted the runtime-cast tax of using object everywhere. Generics give you the reuse without either cost.

Better IntelliSense and tooling. When you write var first = cart[0];, the IDE knows first is a string, not an object, and offers string-specific completions. The signature of Add is Add(string item) instead of Add(object item), so the editor helps you call it correctly. The whole development experience is sharper when the types are precise.

Self-documenting signatures. IReadOnlyDictionary<string, Product> says exactly what it contains and how you can interact with it. The reader doesn't have to chase down what's inside. Compare that with IDictionary (untyped, dictionary of "something to something else"), or a comment that may or may not still be true. Types are documentation that the compiler checks for you.

Generics aren't free at every layer. The CLR creates a separate runtime representation for each value-type construction (one for List<int>, one for List<double>, and so on), so heavily-used generic code over many distinct value types can grow the in-memory footprint of the JIT-compiled code. Reference-type constructions share a single representation, so List<Product> and List<Customer> collapse to the same generated code. This is rarely a concern in application code, but it shows up in libraries that instantiate generics over hundreds of value types.

Two Familiar Faces From the BCL

You've already met two generic types in the _Collections & Data Structures_ section: List<T> and Dictionary<TKey, TValue>. Looking at their signatures with the generics vocabulary in place is a good way to anchor everything we've covered.

List<T> is parameterised over one type, the element type. Dictionary<TKey, TValue> is parameterised over two, the key type and the value type. Both are generic class definitions in the BCL. Both get constructed into closed types (List<string>, Dictionary<string, decimal>) at the use sites. Both store their items in the actual form: strings as references in an array of references, decimals as raw 128-bit values, no boxing involved.

The signatures of their key methods read naturally once you have the vocabulary. List<T>.Add(T item) adds an item of the element type. Dictionary<TKey, TValue>.TryGetValue(TKey key, out TValue value) looks up a value by key. The type parameter names tell you which slot each parameter goes in. This is what good generic API design looks like, and the _Generic Classes_ lesson will show you how to do the same for your own types.