$ sudo teach IT
Модуль 5 · Коллекции

Урок 5.2 — Список (List)

Динамический массив: добавление, удаление, поиск. Главная коллекция C#.

Массив — это здорово, но у него есть серьёзный недостаток: размер фиксирован. А что если ты не знаешь заранее, сколько элементов понадобится? На помощь приходит List<T> — динамический список из пространства имён System.Collections.Generic. Он как массив, но может автоматически расширяться при добавлении новых элементов.

🏗️

Создание списка List<T>

List<T> — это обобщённый (generic) тип. Буква T заменяется на конкретный тип элементов: int, string, double и любой другой.

Список — это ссылочный тип. Он хранится в куче (heap), а переменная содержит ссылку на него. Внутри List использует массив для хранения элементов. Когда массив заполняется, List создаёт новый массив большего размера и копирует туда все элементы.

C# · Создание списка
using System.Collections.Generic;  // необходимо для List

// Пустой список
List<int> numbers = new List<int>();

// Список с начальной ёмкостью (Capacity)
List<string> names = new List<string>(100);  // сразу выделяем место на 100 элементов

// Список с начальными элементами
List<int> primes = new List<int> { 2, 3, 5, 7, 11 };
List<string> fruits = new List<string> { "яблоко", "банан", "вишня" };

// Можно опустить new (C# 9+ target-typed new):
List<double> prices = new() { 99.9, 149.5, 299.0 };
💡 Capacity vs Count: Capacity — сколько элементов может вместить список без выделения новой памяти. Count — сколько элементов реально в списке. Capacity всегда >= Count. При добавлении элемента, когда Count == Capacity, Capacity удваивается.
➕

Добавление элементов: Add, AddRange, Insert

Add() — добавить в конец

Самый частый метод. Добавляет один элемент в конец списка:

C# · Add
List<int> numbers = new List<int>();

numbers.Add(10);  // { 10 }
numbers.Add(20);  // { 10, 20 }
numbers.Add(30);  // { 10, 20, 30 }

Console.WriteLine(numbers.Count);  // 3

AddRange() — добавить коллекцию

Добавляет сразу несколько элементов в конец:

C# · AddRange
List<int> numbers = new List<int> { 1, 2, 3 };
int[] moreNumbers = { 4, 5, 6 };

numbers.AddRange(moreNumbers);   // { 1, 2, 3, 4, 5, 6 }
numbers.AddRange(new[] { 7, 8 }); // { 1, 2, 3, 4, 5, 6, 7, 8 }

// Можно добавить и другой List
List<string> list1 = new() { "a", "b" };
List<string> list2 = new() { "c", "d" };
list1.AddRange(list2);            // { "a", "b", "c", "d" }

Insert() — вставить в середину

Вставляет элемент на указанную позицию. Все элементы сдвигаются вправо.

C# · Insert
List<int> numbers = new List<int> { 10, 20, 40, 50 };

numbers.Insert(2, 30);  // вставить 30 на позицию 2
// Результат: { 10, 20, 30, 40, 50 }

// InsertRange — вставить коллекцию
numbers.InsertRange(0, new[] { 1, 2, 3 });
// Результат: { 1, 2, 3, 10, 20, 30, 40, 50 }
💡 Insert — операция O(n). При вставке в начало каждый раз все элементы сдвигаются. Если нужно часто вставлять в начало — рассмотри LinkedList<T>.
➖

Удаление элементов: Remove, RemoveAt, Clear

Remove() — удалить по значению

Удаляет первое вхождение указанного элемента. Возвращает true, если элемент был найден и удалён:

C# · Remove
List<string> fruits = new() { "яблоко", "банан", "вишня", "банан" };

bool removed = fruits.Remove("банан");  // удаляет ПЕРВЫЙ "банан"
Console.WriteLine(removed);            // True
Console.WriteLine(string.Join(", ", fruits)); // яблоко, вишня, банан

removed = fruits.Remove("арбуз");
Console.WriteLine(removed);            // False — нет такого

RemoveAt() — удалить по индексу

Удаляет элемент на указанной позиции. Остальные элементы сдвигаются влево:

C# · RemoveAt
List<int> numbers = new() { 10, 20, 30, 40, 50 };

numbers.RemoveAt(2);  // удалить элемент с индексом 2 (30)
// Результат: { 10, 20, 40, 50 }

numbers.RemoveAt(0);  // удалить первый
// Результат: { 20, 40, 50 }

// numbers.RemoveAt(10); // ❌ ArgumentOutOfRangeException!

Clear() — очистить всё

Удаляет все элементы из списка. Count становится 0, но Capacity остаётся прежним:

C# · Clear
List<int> numbers = new() { 1, 2, 3, 4, 5 };
Console.WriteLine(numbers.Count);  // 5

numbers.Clear();
Console.WriteLine(numbers.Count);  // 0
Console.WriteLine(numbers.Capacity); // ёмкость сохранилась (например, 8)
🔍

Поиск в списке: Contains, IndexOf, Find, FindAll

Contains() — проверка наличия

Возвращает true, если элемент есть в списке:

C# · Contains
List<string> fruits = new() { "яблоко", "банан", "вишня" };

bool hasApple = fruits.Contains("яблоко");  // true
bool hasMelon = fruits.Contains("дыня");     // false

if (fruits.Contains("банан"))
    Console.WriteLine("Банан есть в списке!");

IndexOf() — поиск индекса

Возвращает индекс первого вхождения элемента. Если не найден — -1:

C# · IndexOf
List<int> numbers = new() { 10, 20, 30, 40, 50, 30 };

int idx = numbers.IndexOf(30);
Console.WriteLine(idx);        // 2 (первое вхождение)

idx = numbers.IndexOf(99);
Console.WriteLine(idx);        // -1 (не найдено)

// Поиск с указанием начального индекса
idx = numbers.IndexOf(30, 3);  // ищем с индекса 3
Console.WriteLine(idx);        // 5

Find() — поиск по условию

Находит первый элемент, подходящий под условие. Если ничего не найдено — возвращает default(T) (0 для int, null для string):

C# · Find
List<int> numbers = new() { 5, 12, 8, 21, 3, 17 };

int firstBig = numbers.Find(n => n > 10);
Console.WriteLine(firstBig);    // 12

int firstEven = numbers.Find(n => n % 2 == 0);
Console.WriteLine(firstEven);   // 12

List<string> names = new() { "Анна", "Борис", "Вика" };
string longName = names.Find(name => name.Length > 4);
Console.WriteLine(longName);    // "Борис"

FindAll() — все подходящие элементы

Возвращает новый список со всеми элементами, удовлетворяющими условию:

C# · FindAll
List<int> numbers = new() { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 };

List<int> evenNumbers = numbers.FindAll(n => n % 2 == 0);
Console.WriteLine(string.Join(", ", evenNumbers)); // 2, 4, 6, 8, 10

List<int> bigNumbers = numbers.FindAll(n => n > 5);
Console.WriteLine(string.Join(", ", bigNumbers));  // 6, 7, 8, 9, 10

// FindAll возвращает новый список — изменения не влияют на оригинал
bigNumbers[0] = 999;
Console.WriteLine(numbers[5]);  // 6 — оригинал не изменился

Exists() и TrueForAll()

Аналогично массивам:

C# · Exists и TrueForAll
List<int> numbers = new() { 1, 2, 3, 4, 5 };

bool hasEven = numbers.Exists(n => n % 2 == 0);  // true
bool allPositive = numbers.TrueForAll(n => n > 0); // true
bool allEven = numbers.TrueForAll(n => n % 2 == 0); // false (есть нечётные)
🔢

Свойство Count и доступ по индексу

List поддерживает доступ по индексу через квадратные скобки, как и массив. Свойство Count — это аналог Length у массивов:

C# · Индексация и Count
List<int> numbers = new() { 10, 20, 30, 40, 50 };

Console.WriteLine(numbers.Count);  // 5
Console.WriteLine(numbers[0]);     // 10
Console.WriteLine(numbers[^1]);    // 50 (C# 8+)
Console.WriteLine(numbers[numbers.Count - 1]); // 50 (classic)

// Изменить по индексу
numbers[2] = 999;
Console.WriteLine(numbers[2]);     // 999
🔄

Сортировка: Sort()

В отличие от массивов (где используется Array.Sort()), у List есть собственный метод Sort():

C# · Sort
List<int> numbers = new() { 5, 2, 8, 1, 9, 3 };
numbers.Sort();
Console.WriteLine(string.Join(", ", numbers)); // 1, 2, 3, 5, 8, 9

List<string> names = new() { "Вика", "Анна", "Борис" };
names.Sort();
Console.WriteLine(string.Join(", ", names));  // Анна, Борис, Вика

// Сортировка по убыванию:
numbers.Sort();
numbers.Reverse();
Console.WriteLine(string.Join(", ", numbers)); // 9, 8, 5, 3, 2, 1

// Или через Comparison:
numbers.Sort((a, b) => b.CompareTo(a));  // по убыванию
🔄

Перебор: for и foreach

Цикл for с индексом:

C# · for
List<int> numbers = new() { 10, 20, 30, 40, 50 };

for (int i = 0; i < numbers.Count; i++)
{
    Console.WriteLine($"numbers[{i}] = {numbers[i]}");
}

Цикл foreach проще и безопаснее:

C# · foreach
foreach (int num in numbers)
{
    Console.WriteLine(num);
}
⚖️

Различия массива и списка

Когда использовать массив, а когда список?

Массив (T[])
Список (List<T>)
Фиксированный размер
Динамический размер (растёт/уменьшается)
Нельзя добавлять/удалять
Add(), Remove(), Insert()
Чуть быстрее (меньше накладных расходов)
Чуть медленнее (проверки, перераспределение)
Свойство: Length
Свойство: Count
Array.Sort(), Array.IndexOf()
list.Sort(), list.IndexOf()
Идеально для фиксированных данных
Идеально для динамических коллекций
💡 На практике List<T> используется гораздо чаще, чем массивы. Массивы применяют в трёх случаях: когда размер точно известен и не меняется; для высокопроизводительного кода; при работе с legacy API, которые возвращают массивы.
🧩

Большой пример — управление списком задач

C# · Менеджер задач
using System;
using System.Collections.Generic;

class Program
{
    static void Main()
    {
        List<string> tasks = new List<string>();
        
        // Добавляем задачи
        tasks.Add("Купить продукты");
        tasks.Add("Сделать зарядку");
        tasks.Add("Выучить C#");
        tasks.Add("Позвонить маме");
        
        Console.WriteLine($"Всего задач: {tasks.Count}");
        
        // Добавляем срочную задачу в начало
        tasks.Insert(0, "СРОЧНО: Оплатить счета");
        
        // Выводим все задачи
        for (int i = 0; i < tasks.Count; i++)
        {
            Console.WriteLine($"{i + 1}. {tasks[i]}");
        }
        
        // Отмечаем задачу как выполненную
        bool done = tasks.Remove("Сделать зарядку");
        Console.WriteLine($"Задача 'Сделать зарядку' выполнена: {done}");
        
        // Ищем задачу по ключевому слову
        string urgent = tasks.Find(t => t.Contains("СРОЧНО"));
        Console.WriteLine($"Срочная задача: {urgent}");
        
        // Добавляем ещё задач через AddRange
        tasks.AddRange(new[] {
            "Почитать книгу",
            "Убраться в комнате",
            "Помочь проекту"
        });
        
        // Сортируем
        tasks.Sort();
        
        Console.WriteLine($"\nСписок после сортировки ({tasks.Count} задач):");
        foreach (string task in tasks)
        {
            Console.WriteLine($"  - {task}");
        }
        
        // Очистка
        // tasks.Clear();
    }
}
📊

Полезные методы List<T>

Метод/Свойство
Описание
Add(item)
Добавить элемент в конец
AddRange(collection)
Добавить коллекцию в конец
Insert(index, item)
Вставить элемент на позицию
Remove(item)
Удалить первый item (возвращает bool)
RemoveAt(index)
Удалить элемент по индексу
Clear()
Очистить весь список
Contains(item)
Проверить наличие элемента
IndexOf(item)
Поиск индекса элемента (-1 если нет)
Find(predicate)
Первый элемент по условию
FindAll(predicate)
Все элементы по условию
Sort()
Сортировка
Reverse()
Переворот порядка
ToArray()
Преобразовать в массив
Count
Количество элементов (свойство)
📌

Что важно запомнить

1️⃣
List<T> — динамический массив. Размер меняется автоматически. Создание: new List<int>() или new List<int> { 1, 2, 3 }.
2️⃣
Add() — добавить в конец, Insert() — вставить, Remove() — удалить по значению, RemoveAt() — по индексу, Clear() — очистить.
3️⃣
Count — количество элементов (аналог Length). Capacity — внутренняя ёмкость. Count <= Capacity. Используй индексацию list[0] и list[^1].
4️⃣
Поиск: Contains() — проверка, IndexOf() — индекс, Find() — по условию, FindAll() — все по условию.
5️⃣
Сортировка: list.Sort() — по возрастанию. Комбинация Sort() + Reverse() — по убыванию.
6️⃣
List vs Array: Список — когда размер неизвестен или меняется. Массив — когда размер фиксирован и нужна максимальная производительность.

Тест: 5.2: Список (List<T>)

3 вопроса

List<T>

Premium