В чем основные различия между массивом (Array) и списком (List) в C#?

Ответ

Массив (T[]) и List<T> — фундаментальные, но принципиально разные коллекции.

Массив (Array)

  • Фиксированный размер. Размер задается при создании и не может быть изменен.
  • Непрерывная память. Элементы хранятся в смежных ячейках памяти, что обеспечивает очень быстрый доступ по индексу (O(1)).
  • Минимальные накладные расходы. Самый "легковесный" вариант коллекции.
  • Ковариантен (для ссылочных типов), что может приводить к ошибкам времени выполнения.
// Создание и инициализация массива
int[] numbersArray = new int[5] { 1, 2, 3, 4, 5 };
// numbersArray[5] = 6; // Выбросит IndexOutOfRangeException

Список (List)

  • Динамический размер. Внутри использует массив, который автоматически пересоздается с увеличенной емкостью (Capacity) при добавлении элементов сверх текущего лимита.
  • Богатый API. Предоставляет методы для удобной работы: Add, Remove, Insert, Find, Sort и многие другие.
  • Контролируемое увеличение. Удвоение Capacity при переполнении (стандартное поведение) — операция дорогая (O(n)), но амортизированная стоимость добавления остается O(1).
  • Инвариантен. Более типобезопасен, чем массив.
// Создание и работа со списком
List<int> numbersList = new List<int>() { 1, 2, 3 };
numbersList.Add(4); // Вместимость (Capacity) увеличится при необходимости
numbersList.Remove(2); // Удалит элемент со значением 2

Сравнительная таблица

Критерий Массив (T[]) List<T>
Размер Фиксированный Динамический
Производительность (доступ по индексу) Максимальная (O(1)) Высокая (O(1), но с косвенным обращением)
Производительность (вставка/удаление) Низкая (требует пересоздания) Средняя (O(n) для середины, O(1) для конца*)
Память Минимум накладных расходов Доп. память на поля класса и запас (Capacity)
Удобство API Базовые операции Богатый набор методов

Когда что использовать?

  • Массив: Когда размер коллекции точно известен и неизменен на всем протяжении работы (например, буфер фиксированного размера, преобразование данных для низкоуровневых API).
  • List<T>: В подавляющем большинстве случаев для работы с изменяемыми коллекциями данных в бизнес-логике благодаря удобству и предсказуемой производительности.

*Примечание: Добавление в конец (Add) имеет амортизированную сложность O(1), но может вызвать дорогостоящее перераспределение внутреннего массива.

Ответ 18+ 🔞

А, ну это классика, вечный спор, как "что лучше — отвёртка или шуруповёрт". Смотри, разжуёшь сейчас.

Массив (T[]) — это как бабушкин сервант. Размер фиксированный, полочки намертво прибиты. Поставил пять хрустальных слоников — на шестой уже места нет, хоть тресни. Зато достаёшь моментально: знаешь, что второй слон стоит на второй полке, хвать — и он у тебя в руках. Памяти жрёт минимум, потому что это просто ряд ячеек вплотную друг к другу. Но есть подвох: если ты объявил массив Animal[], а положил туда Cat[], а потом пытаешься запихнуть Dog — компилятор промолчит, а в рантайме тебе выедет мозг ошибкой. Ковариантность, ёпта.

// Вот, смотри, создал полочку на 5 мест.
int[] numbersArray = new int[5] { 1, 2, 3, 4, 5 };
// Попробуй воткнуть шестую цифру — получишь по ебалу.
// numbersArray[5] = 6; // Вылетит IndexOutOfRangeException, и правильно сделает.

List<T> — это уже сервант с прикрученным двигателем и гидравликой. Внутри у него, по сути, тот же массив, но он хитрожопый. Ты говоришь "добавь", он добавляет. Места нет? Он спокойно так берёт, выпиливает новый сервант в два раза больше, аккуратненько перетаскивает туда все свои коллекционные чашки "За победу КПСС" и живёт дальше. Эта операция — создание нового массива и копирование — дорогая, овердохуища ресурсов, но происходит нечасто, поэтому в среднем добавление в конец — быстрая штука.

У него ещё куча удобных кнопок: "найди", "воткни посередине", "выкинь эту хуйню", "отсортируй по размеру". И он типобезопасный до мозга костей — никаких неожиданных собак в массиве котов.

// А вот лист — он как живой.
List<int> numbersList = new List<int>() { 1, 2, 3 };
numbersList.Add(4); // Добавил, и не чирикнул. Внутри, может, массив пересоздал, но тебе-то похуй.
numbersList.Remove(2); // Искал двойку, выкинул её к чёртовой матери. Проще простого.

Короче, сводная таблица, чтобы в голове отложилось

Критерий Массив (T[]) List<T>
Размер Забили гвоздь в стену — всё, не двигается. Резиновый, как презерватив.
Скорость (доступ по номеру) Максимальная, прямо в память тычется. Почти такая же, но через одну прослойку.
Скорость (вставить/удалить) Пиздец какая низкая, надо всё копировать. В конец — быстро, в середину — придётся всё сдвигать (тоже не сахар).
Память Экономный, как студент до стипендии. Жрёт чуть больше, держит запас (Capacity) на чёрный день.
Удобство "Дай" и "положи". Всё. Целый комбайн с насадками для любой задачи.

Так когда что брать?

  • Массив: Когда ты на 100500% уверен, что размер не поменяется никогда. Ну, типа, дни недели, цвета радуги, координаты вершин куба. Или когда пишешь какую-то низкоуровневую оптимизацию, где каждый такт процессора на счету.
  • List<T>: А это — для всего остального. Серьёзно, в 95% случаев в обычном коде нужен именно он. Динамический, удобный, предсказуемый. Не надо ебаться с ручным управлением памятью.

Запомни: List<T> внутри — это массив в костюме. Он делает всю грязную работу за тебя. А массив — это голый инструмент, острый и быстрый, но без страховки. Выбирай по обстановке, и не выёбывайся с преждевременной оптимизацией.