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

Как работает контейнер Zenject?

2.3 Middle🔥 131 комментариев
#C# и ООП#Паттерны проектирования

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

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

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

Как работает контейнер Zenject (внедрение зависимостей для Unity)

Контейнер Zenject (ныне VContainer или Extenject в более старых версиях) — это мощный IoC-контейнер (Inversion of Control, инверсия управления), предназначенный для управления зависимостями в Unity-проектах. Его основная задача — автоматическое создание объектов и внедрение в них зависимостей, что позволяет писать модульный, тестируемый и гибкий код.

Основные принципы работы

Работа Zenject строится вокруг трёх ключевых концепций:

  1. Привязка (Binding): Регистрация типов в контейнере.
  2. Внедрение (Injection): Автоматическое предоставление зависимостей.
  3. Граф объектов (Object Graph): Контейнер строит и разрешает целые цепочки зависимостей.

1. Процесс привязки (Binding)

Разработчик явно сообщает контейнеру, какой интерфейс (или класс) соответствует какой реализации, и как создавать экземпляры. Это делается в Installer — специальном классе-настройщике.

public class GameInstaller : MonoInstaller
{
    public override void InstallBindings()
    {
        // Привязка интерфейса IWeapon к конкретному классу Sword
        Container.Bind<IWeapon>().To<Sword>().AsSingle();

        // Привязка класса Player с заданием скоупа (области видимости)
        Container.Bind<Player>().FromComponentInNewPrefabResource("Prefabs/Player").AsSingle();

        // Привязка фабрики для создания врагов
        Container.BindFactory<Enemy, Enemy.Factory>().FromComponentInNewPrefabResource("Prefabs/Enemy");
    }
}

Ключевые методы привязки:

  • .Bind<T>() — указывает, какой тип регистрируем.
  • .To<U>() — указывает реализацию.
  • Скоупы (Scopes) определяют жизненный цикл объекта:
    *   `.AsSingle()` — **синглтон** в рамках контейнера (один экземпляр на всё приложение).
    *   `.AsTransient()` — новый экземпляр при каждом запросе.
    *   `.AsCached()` — один экземпляр на одну цепочку разрешения зависимостей.

2. Процесс внедрения (Injection)

После регистрации зависимостей Zenject автоматически находит и внедряет их в целевые объекты. Есть три основных способа:

  • Через конструктор (Наиболее предпочтительный):

    public class Player
    {
        private readonly IWeapon _weapon;
    
        public Player(IWeapon weapon)
        {
            _weapon = weapon; // Zenject сам найдёт и передаст Sword
        }
    }
    
  • Через поля (с атрибутом [Inject]):

    public class Enemy
    {
        [Inject]
        private IWeapon _weapon;
    }
    
  • Через методы (с атрибутом [Inject]):

    public class GameController
    {
        private Player _player;
    
        [Inject]
        public void Construct(Player player)
        {
            _player = player;
        }
    }
    

3. Разрешение зависимостей и построение графа

Когда контейнеру требуется создать объект (например, Player), он анализирует его конструктор и видит зависимость IWeapon. Затем он ищет привязку для IWeapon, находит Sword, и создаёт экземпляр Sword. Если у Sword тоже есть зависимости, процесс повторяется рекурсивно. В итоге строится целый граф взаимосвязанных объектов.

Жизненный цикл и подконтейнеры

  • ProjectContext: Глобальный контейнер, существующий на протяжении всей жизни приложения. Загружается первым при старте.
  • SceneContext: Контейнер уровня (сцены). Управляет зависимостями, специфичными для данной сцены. Может наследовать привязки из ProjectContext.
  • GameObjectContext: Контейнер для отдельного GameObject и его дочерних объектов. Полезен для сложных, изолированных подсистем.

Эта иерархия позволяет эффективно управлять памятью: объекты, привязанные как AsSingle() в SceneContext, будут уничтожены при разгрузке сцены.

Практические преимущества в Unity

  • Упрощение тестирования: Зависимость от IMovementService легко подменить мок-объектом в юнит-тестах.
  • Управление MonoBehaviour: Zenject может инстанцировать и внедрять зависимости в префабы через FromComponentInNewPrefab.
  • Фабрики (Factory) и Пуллы (Pool): Встроенные паттерны для управления созданием и переиспользованием объектов (например, пулы пуль).
  • Сигналы (Signal) (в Extenject): Легковесная альтернатива встроенным событиям Unity, не создающая жестких ссылок.

Пример полного цикла

// 1. Установщик
public class AppInstaller : MonoInstaller
{
    [SerializeField] private PlayerView _playerPrefab;

    public override void InstallBindings()
    {
        Container.Bind<ISaveService>().To<JsonSaveService>().AsSingle();
        Container.Bind<PlayerView>().FromComponentInNewPrefab(_playerPrefab).AsSingle();
        Container.BindInterfacesAndSelfTo<GameManager>().AsSingle();
    }
}

// 2. Класс с зависимостями
public class GameManager : IInitializable
{
    private readonly ISaveService _saveService;
    private readonly PlayerView _player;

    public GameManager(ISaveService saveService, PlayerView player)
    {
        _saveService = saveService;
        _player = player;
    }

    public void Initialize()
    {
        // Автоматически вызывается Zenject после создания всех зависимостей
        Debug.Log("All dependencies are ready!");
    }
}

Таким образом, Zenject выступает в роли центрального "проводника", который знает обо всех компонентах системы и их отношениях. Это кардинально снижает связанность кода, поскольку классы не создают зависимости напрямую, а лишь декларируют свои потребности через конструктор. Вся сложность их сборки перекладывается на конфигурацию контейнера, что делает архитектуру проекта чище, а код — более пригодным для рефакторинга и тестирования.

Как работает контейнер Zenject? | PrepBro