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

Различаешь ли ситуации в которых стоит использовать разные коллекции

1.8 Middle🔥 201 комментариев
#C# и ООП#Коллекции и структуры данных#Оптимизация

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

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

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

Когда и какую коллекцию использовать в Unity/C#

Да, абсолютно. Выбор правильной коллекции — это фундаментальный навык для разработчика Unity, напрямую влияющий на производительность, читаемость кода и отсутствие скрытых багов. В Unity мы работаем преимущественно с коллекциями из пространства имен System.Collections.Generic. Я всегда выбираю коллекцию, исходя из трех ключевых критериев: назначение данных, частота и тип операций (добавление, удаление, поиск, перебор) и требования к производительности в реальном времени (60 FPS).

Вот основные типы коллекций и сценарии их применения в геймдеве:

1. Список (List<T>) — «Рабочая лошадка»

Использую List<T> по умолчанию, когда нужен простой динамический массив с порядком.

  • Когда использовать:
    *   Хранение однотипных объектов, количество которых может меняться (пули, враги в волне, предметы в инвентаре).
    *   Когда важен порядок (очередь спавна, порядок в UI).
    *   Частый доступ по индексу (`list[i]`) и последовательный перебор (`foreach`).
    *   Добавление в конец (`Add`).
  • Когда НЕ использовать:
    *   Частые вставки/удаления в начале или середине — это очень дорого (`O(n)`).
    *   Необходимость быстрого поиска элемента по значению (`Contains` работает за `O(n)`).
    *   Уникальность элементов.

// Пример: Управление пулями
public class BulletManager : MonoBehaviour {
    private List<Bullet> _activeBullets = new List<Bullet>();

    void Update() {
        // Эффективный перебор для обновления
        for (int i = _activeBullets.Count - 1; i >= 0; i--) {
            if (_activeBullets[i].IsDead) {
                _activeBullets.RemoveAt(i); // Удаление с конца - быстро
            } else {
                _activeBullets[i].Update();
            }
        }
    }
}

2. Словарь (Dictionary<TKey, TValue>) — Для мгновенного поиска

Это мой выбор, когда нужен быстрый доступ по уникальному ключу (O(1) в среднем).

  • Когда использовать:
    *   Кэширование ресурсов (ключ — `string` путь, значение — `GameObject` или `Sprite`).
    *   Системы идентификаторов (ID игрока -> данные игрока).
    *   Быстрая проверка наличия элемента (`ContainsKey`).
  • Важные нюансы:
    *   Нет порядка элементов.
    *   Ключ должен быть уникальным и корректно реализовывать `GetHashCode()` и `Equals()`.
    *   Перебор (`foreach`) менее эффективен, чем у `List`.
    *   **Не использовать для данных, изменяемых каждый кадр** (например, `Dictionary` в `Update()` для поиска по изменяющемуся ключу) — это может создать аллокации памяти из-за изменения бакетов.

// Пример: Кэш префабов
public class PrefabCache {
    private static Dictionary<string, GameObject> _cache = new Dictionary<string, GameObject>();

    public static GameObject Load(string path) {
        if (!_cache.ContainsKey(path)) {
            _cache[path] = Resources.Load<GameObject>(path);
        }
        return _cache[path];
    }
}

3. Множество (HashSet<T>) — Для уникальности и операций с множествами

Использую HashSet<T>, когда мне важна уникальность элементов и быстрые операции проверки принадлежности (ContainsO(1)).

  • Когда использовать:
    *   Хранение уникальных ID (обработанные игроки, собранные уникальные предметы).
    *   Быстрая проверка, был ли элемент уже обработан.
    *   Выполнение операций объединения (`UnionWith`), пересечения (`IntersectWith`).
  • Особенность: Как и Dictionary, не сохраняет порядок.

4. Очередь (Queue<T>) и Стек (Stack<T>) — Для определенного порядка обработки

Использую их для моделей FIFO (первый пришел — первый ушел) и LIFO (последний пришел — первый ушел).

  • Queue<T>: Идеален для очередей сообщений, команд, точек пути, создания эффектов с задержкой (пошаговая стратегия).
  • Stack<T>: Полезен для реализации отмены действий (Undo), обхода в глубину в алгоритмах, цепочек состояний.
// Пример: Система команд для юнита
public class CommandQueue {
    private Queue<ICommand> _commands = new Queue<ICommand>();

    public void EnqueueCommand(ICommand cmd) => _commands.Enqueue(cmd);
    public void ProcessNext() {
        if (_commands.Count > 0) {
            _commands.Dequeue().Execute();
        }
    }
}

5. Массив (T[]) — Для максимальной производительности и неизменности

Использую массив в высокопроизводительных или memory-critical контекстах.

  • Когда использовать:
    *   Размер коллекции **известен на этапе компиляции и не меняется** (статические конфиги, сетка вокселей).
    *   Критически важна производительность и минимизация аллокаций в куче (например, в `FixedUpdate` или при работе с `Job System` и `Unity.Collections.NativeArray`).
    *   Взаимодействие с низкоуровневыми API.
  • Недостаток: Фиксированный размер.

Специальный случай для Unity: Transform и поиск дочерних объектов

Никогда не использую GetComponentInChildren или поиск по имени в Update. Вместо этого кеширую ссылки в List<Transform> или массив при инициализации (Awake/Start).

Итоговый алгоритм выбора:

  1. Нужен быстрый поиск по ключу? -> Dictionary.
  2. Нужна уникальность и быстрая проверка "содержит"? -> HashSet.
  3. Данные добавляются/обрабатываются в определенном порядке (FIFO/LIFO)? -> Queue или Stack.
  4. Размер фиксирован и критична производительность? -> Массив.
  5. Во всех остальных случаях (и это большинство) — начинаю с List<T>.

Правильный выбор коллекции предотвращает производительностные узкие места (например, поиск за O(n) в List из 1000 элементов каждый кадр) и делает архитектуру кода более ясной и предсказуемой.