Что значит <T>
Полка из прошлых уроков была с табличкой: на List<int> ставят только числа, на List<string> — только строки. А теперь представьте полку без таблички, куда ставят что угодно: книги, кружки, коробки. Поставить туда вещь нетрудно. Но, снимая вещь с такой полки, каждый раз приходится проверять, что попало в руки.
Такая полка в .NET тоже есть: это старый список ArrayList, который появился раньше List<T>. Он хранит элементы как object — тип, в котором помещается значение любого типа. Что получится, если сложить два его элемента?
using System.Collections;
var items = new ArrayList();
items.Add(5);
items.Add(7);
int sum = items[0] + items[1]; // ✗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. Тип с параметром типа называют обобщённым, а саму эту возможность языка — обобщениями.
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Давайте разберём, что здесь знает компилятор:
- В
List<int>лежат числаint, поэтомуnums[0] + nums[1]складывается без приведений. - В
List<string>лежат строки, поэтомуnames[0]— сразу строка, и у неё естьLength. 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 хватает одного:
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;
}Разберём объявление и вызовы:
<T>после имени метода объявляет параметр типа. ДальшеTпишут везде, где нужен тип: у параметров и у переменнойtmp.refпередаёт методу сами переменные, поэтому обмен виден и после вызова.- В вызове
Swap(ref a, ref b)компилятор сам определяетTпо аргументам: здесьT— этоint, а вSwap(ref x, ref y)—string. - Тип можно указать и явно:
Swap<int>(ref a, ref b).
Частая ошибка
В список чисел попробовали положить строку:
List<int> nums = [1, 2];
nums.Add("three"); // ✗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компилятор определяет по аргументам.