Перейти к основному содержанию
Возможно, вы уже сталкивались с этой штукой под названием IoC. Что же означает эта загадочная и трудно набираемая аббревиатура? Вы попали по адресу.

Что такое IoC?

IoC расшифровывается как Inversion of Control (Инверсия управления). То, что делает наш IoCManager, более конкретно называется Dependency Injection (Внедрение зависимостей). По своей сути внедрение зависимостей означает, что вместо «Я хочу этот конкретный менеджер логирования» вы просто говорите «Я хочу штуку, которая может логировать сообщения для меня». Как это работает? Интерфейсы C#! В приведённом выше примере вы бы не ссылались на статический LogManager, вместо этого вы бы сказали IoC найти что угодно, реализующее ILogManager. Это создаёт более чистый, более тестируемый и поддерживаемый код. Система довольно проста в использовании, если вы понимаете её суть (для чего вы это читаете!), так что не волнуйтесь.

Как мне это использовать?

Прежде всего, IoCManager является static, так что вы можете использовать его откуда угодно. В использовании IoC есть две части: создание зависимости и получение её. Говоря об IoC, «зависимость» — это то, чем он управляет.

Регистрация зависимости

Все зависимости регистрируются вручную в начале программы, то есть в Program.cs для клиента и сервера и SS14UnitTest.cs для проекта модульного тестирования. IoC сопоставляет тип (обычно интерфейс) с конкретной реализацией, которую он может инстанцировать. Конкретная реализация может иметь несколько интерфейсов, но интерфейс может иметь только одну реализацию. Это делается через метод Register<TInterface, TImplementation>(bool overwrite = false), где TImplementation : class, TInterface, new(), в менеджере IoC. По сути, это означает, что вы передаёте два типа: второй будет создан, но должен наследовать или реализовывать первый. Где регистрируются эти типы, зависит от того, находитесь ли вы в контенте/сервере/клиенте и т.д., но все они находятся в единых организованных местах: ClientIoC.cs (движок), ClientContentIoC.cs и т.д. Вы должны быстро уловить шаблон. Итак, чтобы создать зависимость, вам понадобятся две вещи: интерфейс и реализация. Обычно интерфейс называют так же, как реализацию, но с префиксом I по соглашению C#. Итак, если бы я создавал новую зависимость на сервере, я бы сделал так:
// Content.Server/Example/MyDependency.cs
namespace Content.Server.Example;

public class MyDependency : IMyDependency
{
    public void Foo()
    {
        Console.WriteLine("Hello World!");
    }
}

// Content.Server/Interfaces/Example/IMyDependency.cs
namespace Content.Server.Interfaces.Example;

public interface IMyDependency
{
    /// <summary>
    ///     Выводит сообщение в консоль.
    /// </summary>
    void Foo();
}

// ServerContentIoC.cs
IoCManager.Register<IMyDependency, MyDependency>();
И вы сделали свою зависимость доступной миру. Теперь как её получить? Просто!

Получение зависимости

Самый простой способ получения зависимости — использовать IoCManager.Resolve<T>, где T — это интерфейс, использованный в вызове Register<>. В нашем примере выше Resolve<IMyDependency>() вернёт экземпляр MyDependency, и этот экземпляр всегда будет одним и тем же и будет общим для всей программы. Итак, предположим, наш пример хочет логировать, когда вызывается Foo(). Просто!
public class MyDependency : IMyDependency
{
    public void Foo()
    {
        ILogger logger = IoCManager.Resolve<ILogger>();
        logger.info("Hi!");
        Console.WriteLine("Hello World!");
    }
}
Всё просто. Однако это некрасиво. Это доставляет неудобства и имеет серьёзную проблему циклических зависимостей; кроме того, этот метод даже не работает внутри конструкторов зависимостей. Решение, конечно, есть:

Внедрение в поля (Field Injection) и вы

Чтобы решить проблемы с Resolve<T>, IoC Manager может «внедрять» зависимости в поля. Это означает, что две зависимости могут ссылаться друг на друга без проблем. Использование тоже довольно простое. IoC Manager находит все поля, помеченные атрибутом [Dependency] (полностью игнорируя readonly и private), и разрешает тип поля. Это делается после выполнения конструкторов всех зависимостей. После внедрения полей, если ваша зависимость реализует IPostInjectInit, будет вызван IPostInjectInit.PostInject(), если вам нужно связаться с вашими зависимостями. Обратите внимание, что в большинстве случаев у вас всё равно должен быть явный вызов инициализации где-то, так как этот «псевдоконструктор» подвержен состояниям гонки. Итак, возьмём наш пример выше и преобразуем его в превосходный метод. Мы также добавим сообщение в лог при создании экземпляра, хотя воздержитесь от этого на практике:
public class MyDependency : IMyDependency, IPostInjectInit
{
    [Dependency]
    private readonly ILogger logger = default!;

    // Вызывается, когда logger становится доступным.
    public void PostInject()
    {
        logger.info("MyDependency being created!");
        // ВАЖНО: Не делайте так с логгером на самом деле. Это доносит идею, но сломано.
        // Потому что logger ещё не будет должным образом инициализирован, у него нет выходного файла.
        // Таким образом, это сообщение уйдёт в консоль, но не будет записано ни в какие файлы. Это баг.
    }

    public void Foo()
    {
        // Это, конечно, нормально, при условии, что `Foo()` вызывается после того, как `BaseServer` всё настроил.
        logger.info("Hi!");
        Console.WriteLine("Hello World!");
    }
}
Field injection можно запустить вручную, используя IoCManager.InjectDependencies(object). Это делается автоматически для многих динамически создаваемых типов, таких как компоненты сущностей, Entity System и всё, что создаётся через IDynamicTypeFactory.
Последнее изменение 21 июня 2026 г.