Жизненный цикл объектов в DI-контейнерах¶
Жизненный цикл объектов в DI-контейнерах — это совокупность правил и механизмов, определяющих момент создания экземпляра класса, продолжительность его существования, а также момент и способ уничтожения в рамках управления зависимостями. В контексте внедрения зависимостей (Dependency Injection, DI) жизненный цикл управляется не самим объектом, а внешним компонентом — DI-контейнером, который выступает в роли фабрики и реестра сервисов.
¶Основные режимы жизненного цикла
Большинство DI-контейнеров (Spring, ASP.NET Core, Unity, Guice, Laravel) поддерживают три базовых режима, хотя терминология может незначительно различаться.
¶Transient (Транзиентный)
При каждом запросе зависимости контейнер создает новый экземпляр объекта. Это самый «дешёвый» с точки зрения ресурсов режим, но и самый затратный по производительности при частых обращениях. Объект не хранится в контейнере после возврата ссылки, поэтому контейнер не отслеживает его уничтожение. Рекомендуется для легковесных, не имеющих состояния сервисов (например, репозиториев-обёрток над HTTP-клиентами).
¶Scoped (Область видимости)
Экземпляр создается один раз на область (scope). Область обычно привязана к единице работы: HTTP-запросу, пользовательской сессии, транзакции базы данных. В рамках одной области все потребители получают один и тот же экземпляр, но для другой области создается новый. Контейнер хранит ссылку на объект до завершения области, после чего вызывает его освобождение (если реализован интерфейс IDisposable). Это стандартный режим для DbContext в Entity Framework и для UnitOfWork.
¶Singleton (Синглтон)
Экземпляр создается один раз при первом запросе и живет до завершения работы приложения (или до явной остановки контейнера). Все потребители во всех областях получают одну и ту же ссылку. Контейнер гарантирует потокобезопасность создания (обычно через блокировку). Синглтоны должны быть потокобезопасными и не должны зависеть от Scoped-сервисов, так как это приводит к ошибке «Captive Dependency» (захваченная зависимость).
¶Продвинутые сценарии и проблемы
¶Захваченные зависимости (Captive Dependency)
Ситуация, при которой сервис с более длительным жизненным циклом (например, Singleton) внедряет сервис с более коротким (Scoped или Transient). В результате короткоживущий объект «захватывается» и живёт дольше, чем должен, что приводит к утечкам памяти или некорректному состоянию (например, общий DbContext на всё приложение). Современные контейнеры (ASP.NET Core) выбрасывают исключение при такой регистрации.
¶Деструкторы и IDisposable
Контейнер не использует финализаторы (деструкторы C#) для очистки, так как они вызываются сборщиком мусора недетерминированно. Вместо этого контейнер отслеживает объекты, реализующие IDisposable, и вызывает Dispose() в порядке, обратном созданию. Для Transient-объектов контейнер обычно не вызывает Dispose, если не реализована специальная фабрика (например, Func<T> в Autofac).
¶Фабрики и отложенное создание
Для управления сложными сценариями (например, создание объекта по параметру, полученному из рантайма) используются фабричные делегаты (Func<T>, Func<string, T>). Контейнер регистрирует фабрику, а не сам тип, что позволяет отложить создание до момента вызова и передать контекстные данные.
¶Декораторы и перехватчики
Жизненный цикл декоратора (обёртки над сервисом) должен совпадать с жизненным циклом декорируемого сервиса или быть короче. Если декоратор — Singleton, а внутренний сервис — Scoped, возникает захваченная зависимость. Поэтому декораторы часто регистрируются как Transient, даже если оборачивают синглтоны.
¶Управление временем жизни в коде
Контейнер предоставляет методы для явного создания и завершения областей:
CreateScope()— создает новую область (например, для фоновой задачи).GetRequiredService<T>()— запрашивает сервис из текущей области.Dispose()— завершает область, вызываяDisposeу всех Scoped-сервисов.
В веб-приложениях область создается автоматически на каждый HTTP-запрос (middleware). В консольных приложениях разработчик управляет областями вручную.
¶Критика и альтернативы
Некоторые архитекторы считают, что явное управление жизненным циклом через контейнер нарушает принцип прозрачности зависимостей, так как класс не знает, сколько времени он проживёт. Альтернативой является использование «Pure DI» (ручное построение графа объектов без контейнера), где время жизни контролируется кодом верхнего уровня. Однако в крупных проектах контейнеры остаются стандартом де-факто, так как автоматизируют рутинные операции и уменьшают количество шаблонного кода.
¶Источники
- Марк Симан, «Внедрение зависимостей в .NET» (главы о времени жизни).
- Документация Microsoft Learn: «Dependency injection in ASP.NET Core» (разделы о Scoped, Singleton, Transient).
- Документация Spring Framework: «Bean Scopes».
- Стивен ван Деурсен, «Dependency Injection Principles, Practices, and Patterns».