Перейти к содержимому
Теория · Списки, словари и множества · урок 4 из 10

Что значит <T>

Полка из прошлых уроков была с табличкой: на List<int> ставят только числа, на List<string> — только строки. А теперь представьте полку без таблички, куда ставят что угодно: книги, кружки, коробки. Поставить туда вещь нетрудно. Но, снимая вещь с такой полки, каждый раз приходится проверять, что попало в руки.

Такая полка в .NET тоже есть: это старый список ArrayList, который появился раньше List<T>. Он хранит элементы как object — тип, в котором помещается значение любого типа. Что получится, если сложить два его элемента?

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'

Программа не компилируется. В списке лежат числа 5 и 7, но компилятор знает про элементы ArrayList только то, что это object, а складывать два object нельзя.

ArrayList лежит в пространстве имён System.Collections, поэтому файл начинается с директивы using.

Чтобы сложить элементы, каждый приводят к int: (int)items[0]. Если на этом месте окажется строка, ошибка найдётся только при запуске: программа остановится с исключением InvalidCastException.

Тип в угловых скобках

List<T> устроен иначе. Буква T в его имени — параметр типа: место для типа, которое заполняют, когда объявляют список. В List<int> вместо T стоит int, а в List<string> вместо него стоит string. Тип с параметром типа называют обобщённым, а саму эту возможность языка — обобщениями.

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

Давайте разберём, что здесь знает компилятор:

  1. В List<int> лежат числа int, поэтому nums[0] + nums[1] складывается без приведений.
  2. В List<string> лежат строки, поэтому names[0] — сразу строка, и у неё есть Length.
  3. List<int> и List<string> получены из одного обобщённого типа List<T>, но это два разных типа, как две полки с разными табличками.

Здесь картинка с табличкой неточна. Табличку на полке можно переклеить, а тип элементов списка задан раз и навсегда: List<int> никогда не станет List<string>.

Зачем обобщения

На собеседовании часто спрашивают, зачем нужны обобщения, если есть object. Ответ из трёх частей:

  • компилятор проверяет тип элементов: строку в List<int> не положить, и ошибка видна до запуска, а не во время работы программы;
  • приведения не нужны: элемент List<int> — уже int;
  • числа лежат во внутреннем массиве как есть, а ArrayList заворачивает каждое число в отдельную обёртку типа object, и на это уходит время.

Поэтому в новом коде ArrayList не используют: его место занял List<T>.

Свой обобщённый метод

Параметр типа бывает не только у типов, но и у методов. Метод с параметром типа называют обобщённым методом. Вот метод Swap, который меняет местами значения двух переменных. Без обобщений его пришлось бы писать дважды, для int и для string, а с параметром T хватает одного:

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;
}

Разберём объявление и вызовы:

  1. <T> после имени метода объявляет параметр типа. Дальше T пишут везде, где нужен тип: у параметров и у переменной tmp.
  2. ref передаёт методу сами переменные, поэтому обмен виден и после вызова.
  3. В вызове Swap(ref a, ref b) компилятор сам определяет T по аргументам: здесь T — это int, а в Swap(ref x, ref y) — string.
  4. Тип можно указать и явно: Swap<int>(ref a, ref b).

Частая ошибка

В список чисел попробовали положить строку:

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

Программа не компилируется: метод Add у List<int> принимает только int. Это та самая проверка, ради которой придуманы обобщения. В ArrayList строка легла бы без ошибки, а программа сломалась бы позже, при приведении к int. Чтобы добавить число, напишите nums.Add(3).

Главное

  • T в List<T> — параметр типа: в List<int> вместо него стоит int, в List<string> — string.
  • С обобщениями компилятор проверяет тип ещё до запуска, и приведения не нужны: элемент List<int> — сразу int.
  • ArrayList хранит всё как object: неверный тип обнаруживается только при запуске, поэтому в новом коде берут List<T>.
  • Обобщённый метод объявляет свой параметр типа, void Swap<T>(ref T left, ref T right), а T компилятор определяет по аргументам.