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

Список List<T>

Чему вы научитесь

Чему вы научитесь:

  • добавлять, удалять и читать элементы списка List<T>;
  • объяснять, чем Count отличается от Capacity и как растёт список;
  • понимать, что значит <T>, и писать свой обобщённый метод;
  • искать значение по ключу в Dictionary и хранить неповторяющиеся значения в HashSet.
Полка guests с местами 0 1 2 3: книгу вставляют на место 1 по стрелке Insert, и книги справа сдвигаются

Представьте книжную полку. Книги стоят на ней по порядку: новую можно поставить в конец или втиснуть между другими. Когда книгу снимают, соседние сдвигаются. С рядом шкафчиков из темы о массивах так не выйдет: число шкафчиков не меняется.

Зачем программе такая полка? Допустим, вы собираете имена гостей, которые записались на встречу. Сколько их будет, заранее неизвестно. Массив пришлось бы создавать с запасом или каждый раз, когда приходит новый гость, создавать заново, на одно место длиннее.

Для этого в .NET есть список — набор элементов одного типа, в который можно добавлять новые элементы и из которого можно их удалять. Это тип List<T>:

C#
List<string> guests = ["Ann", "Bob"];
guests.Add("Cid");
Console.WriteLine(guests.Count); // 3
Console.WriteLine(guests[2]);    // Cid
guests[0] = "Amy";
Console.WriteLine(string.Join(", ", guests)); // Amy, Bob, Cid

Давайте разберём программу по строкам:

  1. List<string> guests объявляет переменную для списка строк. Тип элементов пишут в угловых скобках: List<string> — список строк, List<int> — список чисел int.
  2. ["Ann", "Bob"] создаёт список из двух элементов. Квадратные скобки со значениями здесь те же, что и у массивов.
  3. guests.Add("Cid") ставит новый элемент в конец списка.
  4. guests.Count — сколько элементов сейчас в списке: 3. У массива это Length, у списка — Count.
  5. guests[2] читает элемент по индексу; индексы, как у массива, начинаются с нуля. Печатается Cid. Квадратные скобки с индексом после имени списка называют индексатором.
  6. guests[0] = "Amy"; заменяет первый элемент.
  7. string.Join склеивает элементы списка через запятую, как элементы массива.

Тип List<T> лежит в пространстве имён System.Collections.Generic. Программа .NET 10 подключает это пространство имён сама, поэтому директива using здесь не нужна.

Как создать список

Список из значений

Значения в квадратных скобках вы уже видели у массивов. Объявление слева задаёт тип такой конструкции, поэтому var guests = ["Ann", "Bob"]; не скомпилируется: как и с массивом, это ошибка CS9176. Пустой список записывают как [].

Пустой список через new

new List<int>() создаёт пустой список. Тип здесь виден справа, поэтому переменную можно объявить через var:

C#
var squares = new List<int>();
for (int i = 1; i <= 4; i++)
{
    squares.Add(i * i);
}
int sum = 0;
foreach (int s in squares)
{
    sum += s;
}
Console.WriteLine(squares.Count); // 4
Console.WriteLine(sum);           // 30

Цикл for добавляет в список квадраты 1, 4, 9 и 16. Цикл foreach обходит список от первого элемента до последнего, как массив, и складывает элементы: 30.

Вставка и удаление

Список умеет то, чего не умеет массив: менять число элементов. Для этого у него есть три метода:

  • Insert(index, value) вставляет элемент на место index, а элементы с этого места и дальше сдвигает вправо;
  • RemoveAt(index) удаляет элемент по индексу, и все элементы после него сдвигаются влево;
  • Remove(value) удаляет первый элемент, равный value, и возвращает true; если такого элемента нет, возвращает false.
C#
List<string> guests = ["Ann", "Bob", "Cid"];
guests.Insert(1, "Dan");
Console.WriteLine(string.Join(", ", guests)); // Ann, Dan, Bob, Cid
guests.RemoveAt(0);
Console.WriteLine(guests[0]);            // Dan
Console.WriteLine(guests.Remove("Bob")); // True
Console.WriteLine(guests.Remove("Zoe")); // False
Console.WriteLine(string.Join(", ", guests)); // Dan, Cid

Разберём по шагам:

  1. Insert(1, "Dan") ставит Dan на место с индексом 1; Bob и Cid сдвигаются вправо.
  2. RemoveAt(0) снимает Ann, и остальные сдвигаются влево: Dan был вторым, а стал первым.
  3. Remove("Bob") находит Bob, удаляет его и возвращает true.
  4. Zoe в списке нет, поэтому Remove ничего не удаляет и возвращает false.

Как на полке, индекс элемента — его место сейчас, а не номер навсегда. Если индекс выходит за границу списка, программа останавливается, как и с массивом, но исключение другое: ArgumentOutOfRangeException, а не IndexOutOfRangeException.

Поиск

У списка есть свои IndexOf и Contains. guests.IndexOf("Cid") работает как Array.IndexOf: возвращает индекс первого совпадения или -1, если совпадения нет. guests.Contains("Cid") отвечает, есть ли Cid в списке: true или false.

Список или массив

Здесь полка и список расходятся. Настоящая полка однажды заполняется, и новую книгу поставить некуда. В список же можно добавлять элементы, пока хватает памяти компьютера: место для них список находит сам.

На собеседовании часто спрашивают, чем список отличается от массива. Ответ из двух частей:

  • длина массива задаётся при создании и потом не меняется;
  • число элементов списка меняется при каждом Add, Insert, Remove и RemoveAt.

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

Элемент удалили из списка прямо внутри foreach:

C#
List<int> nums = [1, 2, 3];
foreach (int n in nums)
{
    if (n == 2)
    {
        nums.Remove(n); // ✗
    }
}
TEXT
Unhandled exception. System.InvalidOperationException: Collection was modified; enumeration operation may not execute.

Программа компилируется, но при запуске останавливается с исключением InvalidOperationException: список изменили во время обхода. foreach этого не допускает, потому что иначе после удаления элементы сдвинулись бы и цикл потерял бы своё место.

Одно известное значение удаляют без цикла: nums.Remove(2);. Чтобы удалить все элементы по условию, обходите список циклом for с конца, от nums.Count - 1 до 0, и удаляйте через RemoveAt(i). Удаление с конца сдвигает только те элементы, которые цикл уже прошёл.

Главное

  • List<T> — список элементов типа T: Add и Insert добавляют элементы, Remove и RemoveAt удаляют.
  • Count — число элементов списка; индексы, как у массива, начинаются с нуля.
  • Insert и RemoveAt сдвигают элементы после указанного места, а Remove возвращает true или false.
  • Пока foreach обходит список, менять список нельзя: программа остановится с исключением InvalidOperationException.