关于依赖注入框架VContainer DIIOC 的学习记录

news2025/4/13 5:18:12

文章目录

  • 前言
  • 一、VContainer核心概念
    • 1.DI(Dependency Injection(依赖注入))
    • 2.scope(域,作用域)
  • 二、练习例子
    • 1.Hello,World!
      • 步骤一,编写一个底类。HelloWorldService
      • 步骤二,编写使用低类的类。GamePresenter
      • 步骤三,编写对应的LitetimeScope,GameLifetimeScope。用来注册C#类依赖
    • 2.补充,控制反转 (IoC)
    • 3.构造函数注入
    • 4.方法注入(其他的和上边一样)
    • 5.字段、属性注入
    • 6.C#complex 类型注册
    • 7.接口注册
    • 8.多接口注册
    • 9.自动注册所有已实现的接口
    • 10.注册所有已实现的接口和具体类型
    • 11.注册生命周期Maker接口
    • 11.注册实例
    • 12.将实例注册为接口
    • 13.注册泛型
    • 14.使用委托注册
    • 15.注册工厂
    • 16.注册仅需运行时参数的 Func<> 工厂方法
    • 17.注册需要容器依赖项和运行时参数的 Func<> 工厂方法
    • 18.MonoBehaviour 注册方式
    • 19.注册 ScriptableObject 配置数据
    • 20.注册集合类型解析
    • 21.注册容器回调方法
  • 三、VContainer 配合 UniTask 使用
    • 1.异步初始化接口
    • 2.异步资源加载


前言

稍微记录一下VContainer依赖注入框架的学习过程。这个框架主要是解决代码的依赖问题,假设有个类A和一个类B,类B需要使用到类A作为依赖,这个时候就会有很强的依赖性,当类A改变代码逻辑的时候,会影响到类B,不符合代码原则,所以要进行依赖倒置原则,这个时候我们一般需要准备一个接口A来抽象一下类B需要用到的功能,这样当类A修改的时候不会影响到类B。这里主要是学习经过框架来进行依赖并且进一步提升自己的代码质量

github:https://github.com/hadashiA/VContainer
文章地址:https://vcontainer.hadashikick.jp

一、VContainer核心概念

1.DI(Dependency Injection(依赖注入))

DI(依赖注入)是 OOP 中的一种通用技术,旨在从代码中删除无关的依赖项。 它为您的对象图带来了可测试性、可维护性、可扩展性或任何类型的可交换性。

在所有的编程范式中,基本设计是弱模块耦合和强模块内聚。 如您所知,OOP(面向对象编程)通过对象来实现。

1.Objects 隐藏了其职责 (封装) 的详细信息。
2.对象将 其职责范围之外的工作转移到其他对象。
实际上,从根本上这样做是有问题的。 如果在类代码中编写委托对象,则意味着源代码级别的紧密耦合。 从类中排除无关依赖项的唯一方法是从外部传入它。

因此,如果你的类从外部接收依赖项,它将需要来自外部的帮助。 DI 是一种技术,它有助于一个地方完全在外部解决依赖关系。

延伸阅读:
曼宁 |.NET 中的依赖关系注入
适用于 Unity 的轻量级 IoC 容器 - Seba 的实验室

2.scope(域,作用域)

scope,域,作用域,可以认为是VContainer当中的一个空间范围,在这个范围内的依赖注入是独立的。一般我们会为场景的预制体设置单独的scope,离开这个scope后对应的DI即不在生成。

每一个scope都有**生命周期(LifeTime)**加入这个scope是通过场景或者预制体创建出来的话,那么这个scope的生命周期会在场景或者预制体销毁之后结束,除此之外还有一个RootScope,就是最核心的scope,通常RootScope是注入到单例上面的。

二、练习例子

1.Hello,World!

在场景中,使用LifetimeScope脚本挂载节点的方式来作为容器并且控制范围。
所以所有LifetimeScope子类来进行注册C#代码的依赖项,在启动场景的时候,Lifetime会自动构建Container并且反派到自己的PlayerLoopSystem。

步骤一,编写一个底类。HelloWorldService

 public class HelloWorldService
    {
        public void Hello()
        {
            DLogger.Log("Hello World");
        }
    }

步骤二,编写使用低类的类。GamePresenter

public class GamePresenter : ITickable
    {
        readonly HelloWorldService helloWorldService;

        public GamePresenter(HelloWorldService helloWorldService)
        {
            this.helloWorldService = helloWorldService;
        }

        public void Tick()
        {
            helloWorldService.Hello();
        }
    }

这里基础ITickable接口,这个接口对应Unity程序中的Uodate,继承实现Marker接口接会自动将这个类型注入到对应GameTimeScope的PlayerLoopSystem里面。

步骤三,编写对应的LitetimeScope,GameLifetimeScope。用来注册C#类依赖

public class GameLifetimeScope : LifetimeScope
{
    protected override void Configure(IContainerBuilder builder)
    {
        builder.Register<HelloWorldService>(Lifetime.Singleton); 
        builder.RegisterEntryPoint<GamePresenter>();
    }
}

这里通过重写LifeimeScope的Configure方法来进行C#依赖项。HelloWorldService是作为单例进行注册。

RegisterEntryPoint() 是用于注册与 Unity 的 PlayerLoop 事件相关的接口的别名。

类似于 Register(Lifetime.Singleton)。作为()

在不依赖 MonoBehaviour 的情况下注册生命周期事件有助于领域逻辑和表示的解耦!

由于LiteTimeScope自身依赖Mono,所以创建节点挂载脚本的方式来进行控制。
在这里插入图片描述
启动脚本后打印如下:
在这里插入图片描述

2.补充,控制反转 (IoC)

IoC (Inversion of Control):将具有控制流责任的对象作为入口点。在简单和传统的编程中,入口点是负责中断用户输入的地方。
创建一个HelloUI脚本用于保存对应的脚本对象。

public class HelloUI : MonoBehaviour
{ 
    #region 自动生成
    public Text m_textStart;
    public Button m_btnHello;
    public void Start()
    {
        RefRoot refRoot = GetComponent<RefRoot>();
        m_textStart = refRoot.GetText(0);
        m_btnHello = refRoot.GetButton(1);
    }
    #endregion 
}

在正常的 Unity 编程中,您可以在 HelloUI 中嵌入逻辑调用,但如果使用的是 DI,则可以将 HelloUI 和任何控制流分开。

namespace MyGame
{
     public class GamePresenter : IStartable
     {
         readonly HelloWorldService helloWorldService;
         readonly HelloScreen helloScreen;  

         public GamePresenter(
             HelloWorldService helloWorldService,
             HelloScreen helloScreen)
         {
             this.helloWorldService = helloWorldService;
             this.helloScreen = helloScreen;
         }

        void IStartable.Start()
        {
            helloScreen.m_btnHello.onClick.AddListener(() => helloWorldService.Hello());
        }
     }    
}

通过这样做,我们成功地分离了域逻辑 / 控制流 / 视图组件。

GamePresenter 的演示者:仅负责 Control Flow。
HelloWorldService 中:只对可以随时随地调用的功能负责
HelloUI:仅负责 View。
在 VContainer 中,您需要注册依赖的 MonoBehaviour。不要忘记注册 HelloUI。

3.构造函数注入

构造函数里,只需要写一个需要依赖注入的函数,成员变量里就可以随时获得对象。如下例子ClassB构造函数的参数是ClassA,我们的classA变量就可以随时使用

class ClassB : IStartable,ITickable
{
    readonly ClassA a;
    public ClassB(ClassA a)
    {
        Debug.Log("ClassA构造函数注入");
        this.a = a;
    }
 
    public void Start()
    {
        a.Start();
    }
 
    public void Tick()
    {
        a.Update();
    }
}

class ClassA
{
    public ClassA()
    {
        Debug.Log("ClassA构造");
    }
 
    public void Start()
    {
        Debug.Log("Start");
    }
    public void Update() 
    {
        Debug.Log("Update");
    }
}
public class GameLifetimeScope : LifetimeScope
{
    //public UIView helloScreen;
 
    protected override void Configure(IContainerBuilder builder)
    {
 
        builder.RegisterEntryPoint<ClassB>();
        builder.Register<ClassA>(Lifetime.Singleton);
    }
}

4.方法注入(其他的和上边一样)

class ClassB : IStartable,ITickable
{
    private ClassA a;
 
    [Inject]
    public void GetClassA(ClassA a) 
    {
        Debug.Log("方法注入");
        this.a = a;
    }
 
    public void Start()
    {
        a.Start();
    }
 
    public void Tick()
    {
        a.Update();
    }
}

5.字段、属性注入

class ClassB : IStartable,ITickable
{
    [Inject]
    private ClassA a;
 
    public void Start()
    {
        a.Start();
    }
 
    public void Tick()
    {
        a.Update();
    }
}

6.C#complex 类型注册

class ServiceA : IServiceA, IInputPort, IDisposable { /* ... */ }

注册具体类型如下

builder.Register<ServiceA>(Lifetime.Singleton);

它可以像这样解析:

class ClassA
{
    public ClassA(ServiceA serviceA) { /* ... */ }
}

7.接口注册

builder.Register<IServiceA, ServiceA>();

它可以像这样解析:

class ClassA
{
    public ClassA(IServiceA serviceA) { /* ... */ }
}

8.多接口注册

builder.Register<ServiceA>(Lifetime.Singleton)
    .As<IServiceA, IInputPort>();

它可以像这样解析:

class ClassA
{
    public ClassA(IServiceA serviceA) { /* ... */ }
}

class ClassB
{
    public ClassB(IInputPort inputPort) { /* ... */ }
}

9.自动注册所有已实现的接口

builder.Register<ServiceA>(Lifetime.Singleton)
    .AsImplementedInterfaces();

它可以像这样解析:

class ClassA
{
    public ClassA(IServiceA serviceA) { /* ... */ }
}

class ClassB
{
    public ClassB(IInputPort inputPort) { /* ... */ }
}

10.注册所有已实现的接口和具体类型

builder.Register<ServiceA>(Lifetime.Singleton)
    .AsImplementedInterfaces()
    .AsSelf();

它可以像这样解析:

class ClassA
{
    public ClassA(IServiceA serviceA) { /* ... */ }
}

class ClassB
{
    public ClassB(IInputPort inputPort) { /* ... */ }
}

class ClassC
{
    public ClassC(ServiceA serviceA) { /* ... */ }
}

11.注册生命周期Maker接口

class GameController : IStartable, ITickable, IDisposable { /* ... */ }
builder.RegisterEntryPoint<GameController>();

关键的区别在于它是否在 PlayerLoopSystem 中运行。Register<GameController>(Lifetime.Singleton).AsImplementedInterfaces()
如果要自定义入口点的异常处理,可以使用以下内容注册回调。

```c
builder.RegisterEntryPointExceptionHandler(ex =>
{
    UnityEngine.Debug.LogException(ex);
    // Additional process ...
});

如果您有多个 EntryPoints,则可以选择使用以下声明作为分组。

builder.UseEntryPoints(entryPoints =>
{
   entryPoints.Add<ScopedEntryPointA>();
   entryPoints.Add<ScopedEntryPointB>();
   entryPoints.Add<ScopedEntryPointC>().AsSelf();
   entryPoints.OnException(ex => ...)
});

这与以下相同:

builder.RegisterEntryPoint<ScopedEntryPointA>();
builder.RegisterEntryPoint<ScopedEntryPointB>();
builder.RegisterEntryPoint<ScopedEntryPointC>().AsSelf();
builder.RegisterEntryPointExceptionHandler(ex => ...);

11.注册实例

// ...
var obj = new ServiceA();
// ...

builder.RegisterInstance(obj);

RegisterIntance总是有一个生命周期,所以它没有参数。
它可以像这样解析:

class ClassA
{
    public ClassA(ServiceA serviceA) { /* ... */ }
}

向 RegisterInstance 注册的实例不由容器管理。
Dispose 不会自动执行。
方法注入不会自动执行

如果您希望容器管理创建的实例,请考虑改用以下内容
register(_ => 实例, …)
RegisterComponent(…)

12.将实例注册为接口

builder.RegisterInstance<IInputPort>(serviceA);

builder.RegisterInstance(serviceA)
    .As<IServiceA, IInputPort>();

builder.RegisterInstance(serviceA)
    .AsImplementedInterfaces();

寄存器特定于类型的参数
如果类型不是唯一的,但您有要在启动时注入的依赖项,则可以使用以下内容:

builder.Register<SomeService>(Lifetime.Singleton)
    .WithParameter<string>("http://example.com");

或者,您可以使用键命名 paramter。

builder.Register<SomeService>(Lifetime.Singleton)
    .WithParameter("url", "http://example.com");

它可以像这样解析:

class SomeService
{
    public SomeService(string url) { /* ... */ }
}

此 Register 仅在注入 时起作用。SomeService

class OtherClass
{
    // ! Error
    public OtherClass(string hogehoge) { /* ... */ }
}

13.注册泛型

class GenericType<T>
{
    // ...
}
builder.Register(typeof(GenericType<>), Lifetime.Singleton);

它可以像这样解析:

class SomeService
{
    public SomeService(GenericType<int> closedGenericType) { /* ... */ }
}

14.使用委托注册

实例创建可以委托给 lambda 表达式或其他方法或类。

builder.Register<IFoo>(_ =>
{
    var foo = new Foo();
    // Do something;
    return foo;
}, Lifetime.Scoped);

它可以像这样解析:

class ClassA
{
    public ClassA(IFoo foo) { /* ...*/ }
}

表达式中可使用的第一个参数是IObjectResolver。通过它,我们可以检索并使用已注册的对象。

builder.Register<IFoo>(container =>
{
    var serviceA = container.Resolve<ServiceA>();
    return serviceA.ProvideFoo();
}, Lifetime.Scoped);

IObjectResolver.Instantiate还可用于生成执行 inject 的游戏对象。

builder.Register(container =>
{
    return container.Instantiate(prefab);
}, Lifetime.Scoped);

15.注册工厂

VContainer 通常会在首次解析时构造已注册的依赖项(已注册的实例除外)。若需更精确地控制依赖项的创建时机,可通过注册并使用工厂函数来实现。
工厂函数是 Func<> 委托,其解析方式与其他依赖项相同。它们可用于随时创建一个或多个其他依赖项。
注意
尽管名为"工厂",但工厂函数既可以返回新创建的对象,也可以返回现有对象。这在需要为相同类型的依赖项映射不同键时非常实用,例如为本地多人游戏中每个玩家分配专属的控制器服务。

以下示例中,依赖项解析仅会发生一次。虽然可以在工厂函数中显式调用 IObjectResolver API,但在此特定 Create() 方法内不会触发依赖解析(这正是构造函数的作用所在)。

class FooFactory
{
    public FooFactory(DependencyA dependencyA)
    {
        this.dependencyA = dependencyA;
    }

    public Foo Create(int b) => new Foo(b, dependencyA);
}
builder.Register<FooFactory>(Lifetime.Singleton); // Registered

// ...

var factory = container.Resolve<FooFactory>(); // Dependency resolution occurs

// ...

var foo1 = factory.Create(1); // No resolution needed here
var foo2 = factory.Create(2); // No resolution needed here
var foo3 = factory.Create(3); // No resolution needed here

虽然如上所述创建工厂类很有用,但简单的工厂也可以通过 lambda 表达式来注册。

(补充说明:这种 lambda 表达式的方式更简洁,适合逻辑简单的工厂场景,避免了单独创建工厂类的开销。)

注意
VContainer 不会自动管理工厂返回对象的生命周期。若工厂返回的是实现了 IDisposable 的对象,需手动处理其释放。这种情况下,使用工厂类是更理想的选择——因为工厂类本身由 VContainer 管理,只要实现 IDisposable 接口,VContainer 就会自动清理其资源。

(补充说明:对于需要资源管理的场景,建议将工厂逻辑封装为类而非 lambda,以利用 VContainer 的生命周期管理能力。)

16.注册仅需运行时参数的 Func<> 工厂方法

如果你的工厂不需要其他依赖项,你可以像这样注册它:

builder.RegisterFactory<int, Foo>(x => new Foo(x));

以下是使用它的方法:

class ClassA
{
    readonly Func<int, Foo> factory;

    public ClassA(Func<int, Foo> factory)
    {
        this.factory = factory;
    }

    public void DoSomething()
    {
        var foo = factory(100);
        // ...
    }
}

17.注册需要容器依赖项和运行时参数的 Func<> 工厂方法

若工厂方法需要其他依赖项,需通过注册接受 IObjectResolver 并返回目标 Func<> 的方式实现:

builder.RegisterFactory<int, Foo>(container => // container 是 IObjectResolver
{
    var dependency = container.Resolve<Dependency>(); // 按作用域解析
    return x => new Foo(x, dependency); // 每次工厂调用时执行
}, Lifetime.Scoped);

此版本需要指定 Lifetime 参数,用于控制内部 Func<> 的生成频率(即外部 Func<> 的调用频率)。

带依赖项的工厂解析方式完全相同,如下所示:

class ClassA
{
    readonly Func<int, Foo> factory;

    public ClassA(Func<int, Foo> factory) => this.factory = factory;

    public void DoSomething()
    {
        var foo = factory.Invoke(100);
        // ...
    }
}

工厂内部可配合使用 IObjectResolver 的扩展方法:

builder.RegisterFactory<CharacterType, CharacterActor>(container =>
{
    return characterType =>
    {
        var characterPrefab = ...
        return container.Instantiate(characterPrefab, parentTransform);
    }
}, Lifetime.Scoped);

工厂方法注册
工厂可注册为能转换为 Func<> 的任何委托(包括普通方法)。无论底层实现多复杂,最终都能以 Func<> 形式使用。

假设存在以下工厂类:

class FooFactory
{
    public Foo Create(int b) => new Foo(b, dependencyA);
    // ...
}

无需了解完整类即可将其作为 Func<> 使用:

builder.Register<FooFactory>(Lifetime.Singleton);
builder.RegisterFactory(container => container.Resolve<FooFactory>().Create, Lifetime.Singleton);

// 使用示例
var factory = container.Resolve<Func<int, Foo>>();
var foo1 = factory(1); 

// 仍可获取原工厂实例(若实现 IDisposable 将随容器释放)
var originalFactory = container.Resolve<FooFactory>(); 

18.MonoBehaviour 注册方式

基本注册方法
通过 LifetimeScope 的 [SerializeField] 注册

[SerializeField]
YourBehaviour yourBehaviour;

// ...
builder.RegisterComponent(yourBehaviour);

注意
RegisterComponent 与 RegisterInstance 类似,区别在于使用 RegisterComponent 注册的 MonoBehaviour 即使未被显式 Resolve 也会进行依赖注入。

从场景中注册

builder.RegisterComponentInHierarchy<YourBehaviour>();

通过预制体实例化注册

builder.RegisterComponentOnNewGameObject<YourBehaviour>(Lifetime.Scoped, "NewGameObjectName");

以接口形式注册组件

builder.RegisterComponentInHierarchy<YourBehaviour>()
    .AsImplementedInterfaces();

指定父级 Transform

// 在指定 Transform 下新建 GameObject
builder.RegisterComponentOnNewGameObject<YourBehaviour>(Lifetime.Scoped)
    .UnderTransform(parent);

// 在指定 Transform 下实例化预制体
builder.RegisterComponentInNewPrefab(prefab, Lifetime.Scoped)
    .UnderTransform(parent);

// 在指定 Transform 下查找已有组件
builder.RegisterComponentInHierarchy<YourBehaviour>()
    .UnderTransform(parent);

或运行时动态查找:

builder.RegisterComponentOnNewGameObject<YourBehaviour>(Lifetime.Scoped)
    .UnderTransform(() => {
        // ...
        return parent;
    });

注册为 DontDestroyOnLoad

// 新建 DontDestroyOnLoad 的 GameObject
builder.RegisterComponentOnNewGameObject<YourBehaviour>(Lifetime.Scoped)
    .DontDestroyOnLoad();

// 实例化 DontDestroyOnLoad 的预制体
builder.RegisterComponentInNewPrefab(prefab, Lifetime.Scoped)
    .DontDestroyOnLoad();

批量注册 MonoBehaviour

builder.UseComponents(components =>
{
    components.AddInstance(yourBehaviour);
    components.AddInHierarchy<YourBehaviour>();
    components.AddInNewPrefab(prefab, Lifetime.Scoped);
    components.AddOnNewGameObject<YourBehaviour>(Lifetime.Scoped, "name");
});

等价于:

builder.RegisterComponent(yourBehaviour);
builder.RegisterComponentInHierarchy<YourBehaviour>();
builder.RegisterComponentInNewPrefab(prefab, Lifetime.Scoped);
builder.RegisterComponentOnNewGameObject<YourBehaviour>(Lifetime.Scoped, "name");

19.注册 ScriptableObject 配置数据

在 Unity 中使用 VContainer 注册 ScriptableObject 配置数据的完整指南:
基础配置类定义
首先定义可序列化的配置数据结构:

[Serializable]
public class CameraSettings
{
    public float MoveSpeed = 10f;
    public float DefaultDistance = 5f;
    public float ZoomMax = 20f;
    public float ZoomMin = 5f;
}

[Serializable]
public class ActorSettings
{
    public float MoveSpeed = 0.5f;
    public float FlyingTime = 2f;
    public Vector3 FlyingInitialVelocity = Vector3.zero;
}

创建 ScriptableObject 资源
创建包含所有游戏设置的 ScriptableObject 主资源:

[CreateAssetMenu(fileName = "GameSettings", menuName = "MyGame/Settings")]
public class GameSettings : ScriptableObject
{
    [SerializeField]
    public CameraSettings cameraSettings;  // 注意修正了变量名大小写一致性

    [SerializeField] 
    public ActorSettings actorSettings;
}

通过 Unity 菜单创建资源:

右键点击 Project 窗口

选择 Create → MyGame → Settings

命名为 “GameSettings” 并配置各参数

在 LifetimeScope 中注册
将创建的 ScriptableObject 资源注册到依赖注入容器:

public class SomeLifetimeScope : LifetimeScope
{
    [SerializeField]
    GameSettings settings;  // 拖拽分配在 Inspector 中创建的 GameSettings 资源

    protected override void Configure(IContainerBuilder builder)
    {
        // 注册各个配置部分
        builder.RegisterInstance(settings.cameraSettings);
        builder.RegisterInstance(settings.actorSettings);
        
        // 也可以注册整个 GameSettings 对象
        builder.RegisterInstance(settings);
    }
}

使用注册的配置
在需要的地方通过构造函数注入使用配置:

public class CameraController
{
    private readonly CameraSettings _settings;
    
    public CameraController(CameraSettings settings)
    {
        _settings = settings;
    }

    public void Update()
    {
        // 使用配置参数
        float speed = _settings.MoveSpeed;
        // ...
    }
}

最佳实践建议
资源管理
I.将 GameSettings 资源放在 Resources 文件夹外
II.通过明确的路径或地址ables系统加载

生命周期
I.使用 RegisterInstance 注册的 ScriptableObject 是单例
II.配置数据通常使用 Singleton 生命周期

模块化设计
I.为不同的系统分离配置类
II.避免一个庞大的全局配置类
III.运行时修改
IV.如需运行时修改配置,考虑:

builder.RegisterInstance(settings.cameraSettings).AsImplementedInterfaces();

测试支持
I.可以为测试创建专门的测试配置资源
II.通过不同的 LifetimeScope 注册不同的配置

这种模式非常适合管理游戏中的各种参数配置,特别是需要设计师频繁调整的数值参数。

20.注册集合类型解析

VContainer 支持通过集合接口自动解析特定类型,用于处理多实现的依赖关系。
基本用法
注册多个实现同一接口的类型后,可通过集合接口一次性解析:

// 注册多个IDisposable实现
builder.Register<IDisposable, A>(Lifetime.Scoped);
builder.Register<IDisposable, B>(Lifetime.Scoped);

解析方式
支持两种集合接口形式注入:

  1. IEnumerable 形式
class ServiceConsumer
{
    public ServiceConsumer(IEnumerable<IDisposable> disposables) 
    {
        // 可遍历所有IDisposable实现
        foreach (var disposable in disposables)
        {
            // ...
        }
    }
}
  1. IReadOnlyList 形式
class ServiceConsumer
{
    public ServiceConsumer(IReadOnlyList<IDisposable> disposables)
    {
        // 可通过索引访问
        var first = disposables[0];
        // ...
    }
}

内部机制说明
此功能主要由框架内部使用(如 ITickable 等标记接口),但开发者也可利用此模式处理多实现场景。

典型应用场景
插件式架构
不同模块实现相同接口,主系统需要收集所有实现

事件处理器
多个事件监听器需要统一管理

策略模式
多种策略实现统一接口,根据上下文选择使用

生命周期注意事项
集合中的元素生命周期保持独立,不受集合接口解析影响。即:
1.如果元素注册为Scoped,其生命周期与所属Scope绑定
2.如果元素注册为Singleton,则保持单例

扩展建议
可通过自定义集合类型实现更精确的控制:

// 注册为自定义集合类型
builder.RegisterInstance(new DisposableCollection(disposables));

// 自定义集合类
public class DisposableCollection : IReadOnlyList<IDisposable>
{
    // 实现接口成员...
}

21.注册容器回调方法

容器构建回调
您可以在容器构建时注册任意回调操作:

builder.RegisterBuildCallback(container => 
{
    // 容器构建完成后执行的逻辑
    var serviceA = container.Resolve<ServiceA>();
    var serviceB = container.Resolve<ServiceB>();
    // ...
});

特性说明:
回调参数使用 IObjectResolver 接口(详情参见容器API文档)
适合执行需要容器完全构建后才能进行的初始化操作
在容器所有注册完成但尚未开始解析前触发

容器销毁回调
注册容器销毁时执行的回调:

builder.RegisterDisposeCallback(container => 
{
    // 容器销毁时执行的清理逻辑
    // 如:释放非托管资源、保存状态等
});

典型应用场景:

资源初始化/清理

builder.RegisterBuildCallback(resolver => {
    resolver.Resolve<AssetLoader>().Preload();
});


builder.RegisterDisposeCallback(resolver => {
    resolver.Resolve<AssetLoader>().ReleaseAll();
});

服务热连接

builder.RegisterBuildCallback(resolver => {
    resolver.Resolve<NetworkService>().Connect();
});

数据持久化

builder.RegisterDisposeCallback(resolver => {
    resolver.Resolve<GameState>().Save();
});

生命周期注意事项:
构建回调按注册顺序执行
销毁回调按注册的逆序执行(类似栈结构)
回调中抛出的异常会中断后续回调执行

高级用法:

// 组合使用构建和销毁回调
builder.RegisterBuildCallback(container => {
    var logger = container.Resolve<ILogger>();
    var timer = new Stopwatch();
    timer.Start();
    
    // 在销毁时记录容器存活时间
    builder.RegisterDisposeCallback(_ => {
        timer.Stop();
        logger.Log($"Container lifetime: {timer.ElapsedMilliseconds}ms");
    });
});

三、VContainer 配合 UniTask 使用

UniTask 是为 Unity 优化的高性能 async/await 解决方案,与 VContainer 深度集成后可实现更强大的异步编程能力。
当项目中安装 com.cysharp.unitask 包后,VCONTAINER_UNITASK_INTEGRATION 编译符号会自动启用,激活以下功能:

1.异步初始化接口

IAsyncStartable 接口

public class FooController : IAsyncStartable
{
    public async UniTask StartAsync(CancellationToken cancellation)
    {
        await LoadSomethingAsync(cancellation);  // 异步加载资源
        await InitializeSubSystems();           // 初始化子系统
        // ...
    }
}

注册方式:

builder.RegisterEntryPoint<FooController>();

与同步版 IStartable 类似,注册后 StartAsync 方法会自动执行。
执行时序控制
PlayerLoop 阶段定制

await UniTask.Yield(PlayerLoopTiming.FixedUpdate);  // 指定在FixedUpdate阶段继续执行

注意:
所有 StartAsync 方法默认在主线程同步执行
不同 PlayerLoop 阶段不会等待异步操作完成

异常处理
完善的错误处理机制

try 
{
    await DangerousOperationAsync();
}
catch (Exception ex)
{
    // 完全支持try/catch语法
}

全局异常监听:

UniTaskScheduler.UnobservedTaskException += ex => 
{
    Debug.LogError($"未捕获的异步异常: {ex}");
};

作用域专属处理器:

builder.RegisterEntryPointExceptionHandler(ex => 
{
    Debug.LogError($"作用域内异常: {ex}");
});

取消Token

public async UniTask StartAsync(CancellationToken cancellation)
{
    await LongRunningTask(cancellation);  // 当LifetimeScope销毁时自动取消
}

2.异步资源加载

与生命周期作用域配合

var extraAsset = await Addressables.LoadAssetAsync<ExtraAsset>(key);

using (LifetimeScope.EnqueueParent(parentScope))
using (LifetimeScope.Enqueue(builder => builder.RegisterInstance(extraAsset))
{
    await SceneManager.LoadSceneAsync("AdditiveScene");
}

最佳实践建议
1.初始化顺序控制

[Inject] IInitializable[] _initializables;  // 依赖注入所有需要同步初始化的服务

2.混合使用同步/异步初始化

public class HybridService : IStartable, IAsyncStartable
{
    public void Start() { /* 同步初始化 */ }
    
    public async UniTask StartAsync(CancellationToken cancellation) 
    { 
        /* 异步初始化 */ 
    }
}

3.资源加载模式

// 推荐在LifetimeScope构建阶段预加载关键资源
builder.RegisterBuildCallback(async resolver => 
{
    var asset = await resolver.Resolve<IAssetLoader>().LoadAsync();
});

这套集成方案特别适合需要复杂异步初始化流程的项目,如:
网络游戏连接流程
大型场景的渐进式加载
需要严格资源管理的应用

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2327457.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

Qt常用控件第一部分

1.控件概述 Widget 是 Qt 中的核⼼概念. 英⽂原义是 "⼩部件", 我们此处也把它翻译为 "控件" . 控件是构成⼀个图形化界⾯的基本要素. 像上述⽰例中的, 按钮, 列表视图, 树形视图, 单⾏输⼊框, 多⾏输⼊框, 滚动条, 下拉框等, 都可以称为 "控件"…

docker存储卷及dockers容器源码部署httpd

1. COW机制 Docker镜像由多个只读层叠加而成,启动容器时,Docker会加载只读镜像层并在镜像栈顶部添加一个读写层。 如果运行中的容器修改了现有的一个已经存在的文件,那么该文件将会从读写层下面的只读层复制到读写层,该文件的只读版本依然存在,只是已经被读写层中该文件…

JMeter接口自动化发包与示例

前言 JMeter接口自动化发包与示例 近期需要完成对于接口的测试,于是了解并简单做了个测试示例&#xff0c;看了看这款江湖上声名远播的强大的软件-Jmeter靠不靠谱。 官网&#xff1a;Apache JMeter - Apache JMeter™ 1简介 Apache-Jmeter是一个使用java语言编写且开源&…

INFINI Console 极限控制台密码忘记了,如何重置?

在使用 INFINI Console&#xff08;极限控制台&#xff09;时&#xff0c;可能会遇到忘记密码的情况&#xff0c;这对于管理员来说是一个常见但棘手的问题。 本文将详细介绍如何处理 INFINI Console 密码忘记的情况&#xff0c;并提供两种可能的解决方案&#xff0c;帮助您快速…

汇编学习之《jcc指令》

JCC&#xff08;Jump on Condition Code&#xff09;指的是条件跳转指令&#xff0c;c中的就是if-else, while, for 等分支循环条件判断的逻辑。它包括很多指令集&#xff0c;各自都不太一样&#xff0c;接下来我尽量将每一个指令的c 源码和汇编代码结合起来看&#xff0c;加深…

从零构建大语言模型全栈开发指南:第四部分:工程实践与部署-4.3.3低代码开发:快速构建行业应用(电商推荐与金融风控案例)

👉 点击关注不迷路 👉 点击关注不迷路 👉 点击关注不迷路 文章大纲 从零构建大语言模型全栈开发指南-第四部分:工程实践与部署4.3.3 低代码开发:快速构建行业应用(电商推荐与金融风控案例)1. 低代码与AI结合的核心价值2. 电商推荐系统案例2.1 技术架构与实现2.2 性能…

基于vue框架的智能服务旅游管理系统54kd3(程序+源码+数据库+调试部署+开发环境)带论文文档1万字以上,文末可获取,系统界面在最后面。

系统程序文件列表 项目功能&#xff1a;用户,景点信息,门票预订,酒店客房,客房预订,旅游意向,推荐景点,景点分类 开题报告内容 基于Vue框架的智能服务旅游管理系统开题报告 一、研究背景与意义 1.1 行业现状与挑战 传统系统局限性&#xff1a;当前旅游管理系统普遍存在信息…

用Python实现TCP代理

依旧是Python黑帽子这本书 先附上代码&#xff0c;我在原书代码上加了注释&#xff0c;更好理解 import sys import socket import threading#生成可打印字符映射 HEX_FILTER.join([(len(repr(chr(i)))3) and chr(i) or . for i in range(256)])#接收bytes或string类型的输入…

MySQL的进阶语法7(索引-B+Tree 、Hash、聚集索引 、二级索引(回表查询)、索引的使用及设计原则

目录 一、索引概述 1.1 基本介绍 1.2 基本演示 1.3 特点及优势 二、索引结构 2.1 概述 2.2 二叉树 2.3 B-Tree 2.4 BTree 2.5 Hash 2.5.1 结构 2.5.2 特点 2.5.3 存储引擎支持 三、索引的分类 3.1 索引分类 3.2 聚集索引和二级索引 3.2.1 聚集索引和二级…

【CSS3】04-标准流 + 浮动 + flex布局

本文介绍浮动与flex布局。 目录 1. 标准流 2. 浮动 2.1 基本使用 特点 脱标 2.2 清除浮动 2.2.1 额外标签法 2.2.2 单伪元素法 2.2.3 双伪元素法(推荐) 2.2.4 overflow(最简单) 3. flex布局 3.1 组成 3.2 主轴与侧轴对齐方式 3.2.1 主轴 3.2.2 侧轴 3.3 修改主…

论坛系统的测试

项目背景 论坛系统采用前后端分离的方式来实现&#xff0c;同时使用数据库 来处理相关的数据&#xff0c;同时将其部署到服务器上。前端主要有7个页面组成&#xff1a;登录页&#xff0c;列表页&#xff0c;论坛详情页&#xff0c;编辑页&#xff0c;个人信息页&#xff0c;我…

宠物店小程序怎么做?助力实体店实现营销突破

宠物店小程序怎么做&#xff1f;助力实体店实现营销突破 ——一个宠物店老板的“真香”实战分享 ​一、行业现状&#xff1a;线下宠物店的“流量焦虑”​ 作为开了3年宠物店的“铲屎官供应商”&#xff0c;这两年明显感觉生意难做了&#xff1a;某宝9.9包邮的狗粮、某团“满…

《Mycat核心技术》第21章:高可用负载均衡集群的实现(HAProxy + Keepalived + Mycat)

作者&#xff1a;冰河 星球&#xff1a;http://m6z.cn/6aeFbs 博客&#xff1a;https://binghe.gitcode.host 文章汇总&#xff1a;https://binghe.gitcode.host/md/all/all.html 星球项目地址&#xff1a;https://binghe.gitcode.host/md/zsxq/introduce.html 沉淀&#xff0c…

深度学习Note.5(机器学习.6)

1.Runner类 一个任务应用机器学习方法流程&#xff1a; 数据集构建 模型构建 损失函数定义 优化器 模型训练 模型评价 模型预测 所以根据以上&#xff0c;我们把机器学习模型基本要素封装成一个Runner类&#xff08;加上模型保存、模型加载等功能。&#xff09; Runne…

从零开始设计Transformer模型(1/2)——剥离RNN,保留Attention

声明&#xff1a; 本文基于哔站博主【Shusenwang】的视频课程【RNN模型及NLP应用】&#xff0c;结合自身的理解所作&#xff0c;旨在帮助大家了解学习NLP自然语言处理基础知识。配合着视频课程学习效果更佳。 材料来源&#xff1a;【Shusenwang】的视频课程【RNN模型及NLP应用…

【 <二> 丹方改良:Spring 时代的 JavaWeb】之 Spring Boot 中的缓存技术:使用 Redis 提升性能

<前文回顾> 点击此处查看 合集 https://blog.csdn.net/foyodesigner/category_12907601.html?fromshareblogcolumn&sharetypeblogcolumn&sharerId12907601&sharereferPC&sharesourceFoyoDesigner&sharefromfrom_link <今日更新> 一、开篇整…

华为配置篇-BGP实验

BGP 一、简述二、常用命令总结三、实验 一、简述 IBGP 水平分割&#xff1a;从一个 IBGP 对等体学到的路由&#xff0c;不会再通告给其他的 IBGP 对等体。在一个 AS 内部&#xff0c;路由器之间通过 IBGP 交换路由信息。如果没有水平分割机制&#xff0c;当多个路由器之间形成…

vue element-ui 工程创建

vue element-ui 工程创建 按照步骤 &#xff1a; https://blog.csdn.net/wowocpp/article/details/146590400 创建工程 vue create demo3 cd demo3 npm run serve 在demo3 目录里面 执行如下命令 npm install element-ui -S 然后查看 package.json main.js 添加代码&…

《AI大模型应知应会100篇》第2篇:大模型核心术语解析:参数、Token、推理与训练

第2篇&#xff1a;大模型核心术语解析&#xff1a;参数、Token、推理与训练 摘要 本文将用通俗易懂的语言拆解大模型领域的四大核心概念&#xff1a;参数、Token、训练与推理。通过案例对比、代码实战和成本计算&#xff0c;帮助读者快速掌握这些术语的底层逻辑与实际应用价值…

【28BYJ-48】STM32同时驱动4个步进电机,支持调速与正反转

资料下载&#xff1a;待更新。。。。 先驱动起来再说&#xff0c;干中学&#xff01;&#xff01;&#xff01; 1、实现功能 STM32同时驱动4个步进电机&#xff0c;支持单独调速与正反转控制 需要资源&#xff1a;16个任意IO口1ms定时器中断 目录 资料下载&#xff1a;待更…