← Назад к вопросам

В каком случае List пересоздаётся

2.2 Middle🔥 161 комментариев
#Коллекции и структуры данных#Управление памятью

Комментарии (1)

🐱
deepseek-v3.2PrepBro AI6 апр. 2026 г.(ред.)

Ответ сгенерирован нейросетью и может содержать ошибки

Механизм работы List<T> и его пересоздание

List<T> в C# (который используется в Unity) является динамическим массивом, что означает, что он автоматически управляет своей внутренней структурой данных для размещения элементов. Понимание того, когда List пересоздаётся (то есть полностью заменяет внутренний массив), критически важно для оптимизации производительности, особенно в играх, где частое пересоздание может вызвать сборку мусора (garbage collection) и просадки FPS.

Внутреннее устройство List<T>

Класс List<T> внутри использует обычный массив T[] для хранения элементов. Изначально этот массив имеет некоторую начальную ёмкость (по умолчанию 0 или 4, в зависимости от конструктора). При добавлении элементов, если места в текущем массиве не хватает, происходит пересоздание внутреннего массива с увеличенным размером.

// Пример, демонстрирующий внутренний механизм
List<int> numbers = new List<int>(); // Внутренний массив, допустим, capacity = 0
numbers.Add(1); // Если capacity == 0, выделяется массив capacity = 4
numbers.Add(2);
numbers.Add(3);
numbers.Add(4); // Массив заполнен, capacity = 4
numbers.Add(5); // Происходит пересоздание! Создаётся новый массив, capacity увеличивается (например, до 8)

Случаи, когда List<T> пересоздаётся

1. Превышение текущей ёмкости (Capacity) при добавлении элементов

Это наиболее частая причина. Когда метод Add(), AddRange(), Insert() или InsertRange() вызывается, и текущее количество элементов (Count) равно ёмкости (Capacity), система выделяет новый внутренний массив большего размера (обычно увеличивая ёмкость вдвое, но не всегда), копирует в него старые элементы и затем добавляет новые. Старый массив становится мусором, который будет собран сборщиком.

List<GameObject> enemies = new List<GameObject>(2); // capacity = 2
enemies.Add(enemy1); // count = 1, capacity = 2
enemies.Add(enemy2); // count = 2, capacity = 2 (массив заполнен)
enemies.Add(enemy3); // count = 3 -> capacity становится 4, массив пересоздаётся и копируется!

2. Явное изменение свойства Capacity

Если вы напрямую увеличиваете свойство Capacity до значения, большего текущего, внутренний массив пересоздаётся под новый размер. Уменьшение Capacity (если новое значение меньше текущей Capacity, но не меньше Count) также может привести к пересозданию меньшего массива.

List<string> items = new List<string>(10);
items.Capacity = 100; // Массив пересоздаётся с capacity = 100, даже если элементов мало

3. Вызов метода TrimExcess()

Этот метод пытается уменьшить Capacity до фактического количества элементов (Count). Если разница между Capacity и Count значительна (больше 10% по умолчанию), TrimExcess() пересоздаёт внутренний массив, чтобы он точно соответствовал Count.

List<Vector3> points = new List<Vector3>(1000);
// ... добавлено 10 элементов
points.TrimExcess(); // capacity может уменьшиться с 1000 до 10, массив пересоздаётся

4. Некоторые операции, изменяющие размер

Методы, которые массово изменяют размер коллекции, такие как AddRange(), если добавляемый диапазон большой, могут вызвать одно или несколько пересозданий, пока Capacity не станет достаточной для размещения всех новых элементов.

Как избежать частого пересоздания и оптимизировать работу

  • Предустановка Capacity (ёмкости): Если вы знаете или можете оценить максимальное количество элементов, заранее установите Capacity. Это предотвратит множественные пересоздания при постепенном добавлении.

    // Оптимизированный подход
    int expectedEnemyCount = 50;
    List<Enemy> enemyList = new List<Enemy>(expectedEnemyCount);
    for (int i = 0; i < expectedEnemyCount; i++) {
        enemyList.Add(SpawnEnemy()); // Ни одного пересоздания, если не выйдем за 50
    }
    
  • Использовать пулы объектов (Object Pooling): В Unity вместо постоянного создания и удаления элементов списка (например, пуль) используйте пул. Вы заранее создаёте список объектов и переиспользуете их, что сводит к нулю операции добавления/удаления, требующие изменения List.

  • Избегать ненужного изменения Capacity: Не изменяйте Capacity без необходимости, особенно в часто вызываемых методах (например, в Update()).

  • С осторожностью использовать TrimExcess(): Вызывайте его только тогда, когда вы уверены, что список больше не будет расти, и освобождение памяти важнее потенциального будущего пересоздания.

Итог: Основной сценарий пересоздания List<T> — исчерпание Capacity при добавлении. Для достижения плавной производительности в Unity ключевой стратегией является предварительное выделение достаточной ёмкости и использование пулов объектов, что минимизирует динамическое изменение размеров списка и последующую сборку мусора.