Skip to content
Theory · Lists, dictionaries and sets · lesson 4 of 10

What <T> means

The shelf from the earlier lessons had a label: only numbers go on a List<int>, only strings on a List<string>. Now picture a shelf with no label, where anything goes: books, mugs, boxes. Putting a thing there is no trouble. But each time you take a thing off such a shelf, you have to check what you are holding.

.NET has such a shelf too: the old list ArrayList, which came before List<T>. It stores elements as object, a type that holds a value of any type. What happens if you add up two of its elements?

C#
using System.Collections;

var items = new ArrayList();
items.Add(5);
items.Add(7);
int sum = items[0] + items[1]; // ✗
TEXT
error CS0019: Operator '+' cannot be applied to operands of type 'object' and 'object'

The program does not compile. The list holds the numbers 5 and 7, but the compiler knows only that the elements of an ArrayList are object. And two object values cannot be added.

ArrayList lives in the namespace System.Collections, so the file starts with a using directive.

To add the elements, each one is cast to int: (int)items[0]. If a string turns out to be in that place, the error is found only when the program runs: it stops with an InvalidCastException.

The type in angle brackets

List<T> works differently. The letter T in its name is a type parameter: a slot for a type that is filled in when the list is declared. In List<int> the T is int, in List<string> it is string. A type with a type parameter is called generic, and the language feature itself is called generics.

C#
List<int> nums = [5, 7];
List<string> names = ["Ann", "Bob"];
int sum = nums[0] + nums[1];
string first = names[0];
Console.WriteLine(sum);          // 12
Console.WriteLine(first.Length); // 3

Let's see what the compiler knows here:

  1. A List<int> holds int numbers, so nums[0] + nums[1] adds up with no casts.
  2. A List<string> holds strings, so names[0] is a string right away, and it has Length.
  3. List<int> and List<string> come from one generic type, List<T>, but they are two different types, like two shelves with different labels.

Here the label picture is off. A label on a shelf can be replaced, but a list's element type is set once and for all: a List<int> never becomes a List<string>.

Why generics

Interviewers often ask why generics are needed when there is object. The answer has three parts:

  • the compiler checks the element type: a string cannot go into a List<int>, and the error shows before the program runs, not while it works;
  • no casts are needed: an element of a List<int> is already an int;
  • the numbers sit in the internal array as they are, while ArrayList wraps each number in a separate object, and that takes time.

That is why new code does not use ArrayList: List<T> has taken its place.

A generic method of your own

Methods can have a type parameter too, not only types. A method with a type parameter is called a generic method. Here is a method Swap that swaps the values of two variables. Without generics it would have to be written twice, for int and for string, but with the parameter T one is enough:

C#
int a = 1;
int b = 2;
Swap(ref a, ref b);
Console.WriteLine($"{a} {b}"); // 2 1
string x = "left";
string y = "right";
Swap(ref x, ref y);
Console.WriteLine($"{x} {y}"); // right left

void Swap<T>(ref T left, ref T right)
{
    T tmp = left;
    left = right;
    right = tmp;
}

Let's go through the declaration and the calls:

  1. <T> after the method name declares a type parameter. From there on, T is written wherever a type is needed: for the parameters and for the variable tmp.
  2. ref hands the method the variables themselves, so the swap is still visible after the call.
  3. In the call Swap(ref a, ref b) the compiler works out T from the arguments by itself: here T is int, and in Swap(ref x, ref y) it is string.
  4. The type can also be given explicitly: Swap<int>(ref a, ref b).

Common mistake

Someone tried to put a string into a list of numbers:

C#
List<int> nums = [1, 2];
nums.Add("three"); // ✗
TEXT
error CS1503: Argument 1: cannot convert from 'string' to 'int'

The program does not compile: the Add method of a List<int> takes only an int. This is the very check generics were made for. An ArrayList would have taken the string with no error, and the program would have broken later, at the cast to int. To add a number, write nums.Add(3).

Key points

  • T in List<T> is a type parameter: in List<int> its place is taken by int, in List<string> by string.
  • Generics give a type check at compile time and remove the casts: an element of a List<int> is an int right away.
  • ArrayList stores everything as object: a wrong type shows up only when the program runs, so new code uses List<T>.
  • A generic method declares its own type parameter, void Swap<T>(ref T left, ref T right), and the compiler works out T from the arguments.