Unity高性能依赖注入框架Extenject(Zenject)-----Zenject概念和API的基本使用

一.简介及环境配置

Extenject介绍

 一旦学习了如何使用DI编写正确的松耦合代码,就再也没有回头路了。

Extenject是一个轻量级高性能依赖注入框架,专门针对Unity 3D构建(但是也可以在Unity外部使用)。它可用于将您的应用程序转变为具有高度职责细分的弱耦合模块的集合,通过Extenject框架可以将各个功能模块粘合在一起,从而使您能够以可伸缩且极其灵活的方式轻松编写,重用,重构和测试代码。在Unity3D中使用Extenject框架编程,可以将程序发布到以下平台上:

  • PC/Mac/Linux
  • IOS
  • Android
  • WebGL
  • PS4(支持IL2CPP后端)
  • Window应用商店
  • 所有支持IL2CPP的平台

Zenject特点

  1. Injection
    支持普通C#类 和 继承自Monobehaviors的类
    构造函数注入(Constructor injection)
    成员变量注入(Field injection)
    属性注入(Property injection)
    方法注入(Method injection)
  2. 可通过类型(Type)或名称(Name)完成条件绑定(Conditional binding)
  3. 可选依赖项(Optional dependencies)
  4. 支持使用工厂模式,完成工厂初始化后创建对象
  5. 支持Sub_Container嵌套
  6. 允许跨场景传递注入的信息
  7. 允许一个场景继承另一个场景的绑定
  8. 支持全局全项目的绑定,为所有场景添加依赖
  9. 支持基于约定的绑定,如基于类名、命名空间或其他条件
  10. 支持编辑器模式验证Object Graphs
  11. 支持通过ZenjectBinding组件完成自动绑定
  12. Auto-Mocking的Moq library提供内置内存池支持
  13. Decorator Bindings 支持装饰器模式
  14. 支持自动映射开放的泛型类型
  15. 内置对单元测试,集成测试和场景测试的支持
  16. 通过 LazyInject<>结构支持Just-in-time 注入
  17. 支持多线程resolving/instantiating
  18. 支持Reflection Baking
  19. 使用 ZenAutoInjecter 组件可完成自动注入

如何获取安装插件

Extenject是Zenject项目的延续,是一款免费的插件。可通过如下方式导入:

导入的插件中包含如下资源:

  • Zenject主体
  • 附加功能:
    Zenject测试框架(Zenject Test Framework)
    创建Test Doubles的扩展(Extensions for creating test doubles)
    内存池监视器(Memory Pool Monitor)
    反射烘焙(Reflection Baking)
    信号事件系统(Signals event system)
    Demo01 入门级游戏示例教程
    Demo02 高级游戏示例教程

在这里插入图片描述

二. 依赖注入的概念

官方案例说明

在编写单个类以实现某些功能时,可能需要与系统中的其他类进行交互才能实现其目标。一种方法是让类本身通过调用具体的构造函数来创建其依赖项:

public class Foo
{
    ISomeService _service;
    public Foo()
    {
        _service = new SomeService();
    }
    public void DoSomething()
    {
        _service.PerformTask();}
}

这对于一些规模比较小的项目来说颇为实用,但随着项目的增长,它开始变得笨拙。Foo 类和SomeService类高度耦合,当我们需要修改Service的具体实现时,必须同时而修改Foo类中的代码。而基于单一职责原则,Foo类不应该随着Service的具体实现而发生变动。Foo类更应该关注自身的职责,提供一个接口,只要提供满足这个抽象接口的Service,Foo的功能都可以实现。现在代码发生了变化:

public class Foo
{
    ISomeService _service;

    public Foo(ISomeService service)
    {
        _service = service;
    }

    public void DoSomething()
    {
        _service.PerformTask();
        ...
    }
}

这样看就舒服多了,但是使用Foo类对象的其他类(Bar类)同样面临着填充依赖项的问题

public class Bar
{
    public void DoSomething()
    {
        var foo = new Foo(new SomeService());
        foo.DoSomething();
        ...
    }
}

同样Bar类不关心Foo类使用了哪种Service的具体实现。

public class Bar
{
    ISomeService _service;

    public Bar(ISomeService service)
    {
        _service = service;
    }

    public void DoSomething()
    {
        var foo = new Foo(_service);
        foo.DoSomething();
        ...
    }
}

因此,在一个应用程序中应该具有一个’Object Graph’视图,类关注自身的职责的同时使用着其他类的功能,却不关心其他类的具体实现。举一个极端的例子,在到达应用程序的入口点之前,所有的依赖关系应该都被满足。依赖注入的术语称之为“composition root”;如下所示:(个人理解:在实例化一个类之前必须实例化其所依赖的类,按从底层到高层的顺序实例化,最高层称之为根)

var service = new SomeService();
var foo = new Foo(service);
var bar = new Bar(service);
var qux = new Qux(bar);
.. etc.

诸如Zenject之类的DI框架只是简单地帮助自动化创建和分发这些具体依赖项,因此您无需像上面的代码中那样显式地这样做。

使用Zenject的错误观念

存在很多关于依赖注入的错误认识,骤然改变编程观念是很难的,需要时间和经验来慢慢加深理解。
正如上面的例子所显示的,使用DI可以轻松的改变上层模块使用接口的不同实现(示例中的接口为ISomeService),这只是DI众多优势中的一点。
比这更重要的是,使用像Zenject这样的依赖注入框架可以使您更轻松地遵循“ 单一责任原则 ”。通过让Zenject连接类,类本身可以只专注于履行其特定职责。
一些DI新手常犯一个常见错误,为每个类提取接口,在其他类中使用这些接口,而不是直接使用该类。目标是使代码更松散地耦合,认为绑定到接口比绑定到具体类更好。但是,在大多数情况下,应用程序的各种职责只有一个特定的类来实现它们,因此在这些情况下使用接口只会增加不必要的维护开销。同样,具体类已经具有由其公共成员定义的接口。经验告诉我们仅在类具有多个实现时或在将来打算具有多个实现的情况下才创建接口(重复使用抽象原则)

使用依赖注入(DI)的其他优势:

  • 便于重构。
  • 促近模块化编程
  • 便于测试(单元测试、用户驱动测试)

SelfDemo

当开发小项目小程序时,依赖注入的使用可能不会减少工作量,随着项目越来越大,逻辑日渐复杂,依赖注入的优势则开始显现出来。依赖注入最大的能力是解耦。其中隐含了依赖倒置原则的思想:程序要依赖于抽象接口,不要依赖于具体实现。这样写代码可以更好的进行单元测试,更快的完成代码的迭代更新,便于维护,有利于减少相互耦合造成的意外错误。
笔者认为该框架设计设计了一些设计原则和:

单一职责原则
控制反转(IoC):一种反转流、依赖和接口的方式(DIP的具体实现方式)。
依赖注入(DI):IoC的一种实现方式,用来反转依赖(IoC的具体实现方式)。
IoC容器:依赖注入的框架,用来映射依赖,管理对象创建和生存周期(DI框架)

一篇文章详细的介绍了DIP、IoC、DI以及IoC容器的概念。

对一个大型的项目,我们往往会提出一些期待。我们希望项目的开发是模块化的,而模块又是相互独立的,当某一个模块的需求改变时,最优的方案是用新的模块替换掉淘汰的模块,对修改关闭,对扩展开放;即便在不得已的情况下,只能在原模块的基础上进行修改,也希望这个模块的修改被控制在该模块的范围内,即只需要修改本模块的代码,不修改使用了这个模块的上层代码。反之,一旦修改了底层模块,所以依赖这个底层模块的上层模块都要进行修改,那带来的工作量将是灾难性的。

使用依赖注入和不使用对比

举个例子,我们现在手里有不同品牌的IMU传感器,每个IMU传输数据的方式不同,但提供的数据都是雷同的,假设每个品牌的IMU都可以提供三个方向的加速度,关于姿态的四元数。

通过没有使用依赖注入的代码可以看到,程序被划分为了两个模块,IMU数据读取模块,数据处理模块,其中数据处理模块高度依赖于数据数据读取模块,两者是紧密耦合的,当使用的IMU传感器修改时,我们需要同时修改数据处理模块。

没有使用依赖注入的代码:

using UnityEngine;
public class WithoutDI : MonoBehaviour
{
    void Start()
    {
        DataProcess dataProcess=new DataProcess();
        dataProcess.ProcessDataA();
        dataProcess.ProcessDataB();
    }
    
}
public class DataProcess
{ 
    public void ProcessDataA()
    {
        string data=new IMUReaderA().Read();
        Debug.Log(data);
    }
    public void ProcessDataB()
    {
        string data=new IMUReaderB().Read();
        Debug.Log(data);
    }
    
}
public class IMUReaderA
{
    public string Read()
    {
        return "IMU读取的数据A";
    }
}

public class IMUReaderB
{
    public string Read()
    {
        return "IMU读取的数据B";
    }
}

运行结果:
在这里插入图片描述
在使用构造函数注入的依赖注入模式实现控制反转之后,当使用的IMU发生改变后,不需要修改数据处理模块的代码,仅仅需要重新实现读取模块即可,也便于在不同IMU传感器间进行切换。每个类具有特定的职责,且仅完成该职责,只要满足上层模块定义的接口需求,就可以完成模块的替换。

使用了控制反转依赖注入的代码:

using UnityEngine;
public class WithDI : MonoBehaviour
{
    void Start()
    {
        DataProcess2 dataProcess2=new DataProcess2();
        dataProcess2.ProcessData(new IMUReaderC());
        dataProcess2.ProcessData(new IMUReaderD());
    }
}
public class DataProcess2
{ 
    public void ProcessData(IIMUReader reader)
    {
        string data = reader.Read();
        Debug.Log(data);
    }
}
public interface IIMUReader
{
    string Read();
}

public class IMUReaderC:IIMUReader
{
    public string Read()
    {
        return "IMU读取的数据C";
    }
}
public class IMUReaderD:IIMUReader
{
    public string Read()
    {
        return "IMU读取的数据D";
    }
}

在这里插入图片描述

依赖注入容器

这是一个非常简单的示例程序,只需要注入一个实现读取数据的类,在项目的开发过程中,往往需要许多的接口和实现,此时我们需要一个依赖注入容器来管理所有的依赖项。

三.Zenject API

Demo “Hello World”

Binding

为了获得对于Zenject使用的整体把握,以一个简单的Hello world示例作为入门。

using Zenject;
using UnityEngine;
using System.Collections;

public class TestInstaller : MonoInstaller
{
    public override void InstallBindings()
    {
        Container.Bind<string>().FromInstance("Hello World!");
        Container.Bind<Greeter>().AsSingle().NonLazy();
    }
}

public class Greeter
{
    public Greeter(string message)
    {
        Debug.Log(message);
    }
}

运行上述代码需要完成这些步骤:

  1. 创建一个Unity工程,导入Zenject插件(Extenject)并创建一个新的场景(Scene)

  2. 在Hierarchy区域通过右击创建一个SenceContext在这里插入图片描述

  3. 在Project中的文件夹里新建一个Monoinstaller 命名为 TestInstaller.cs在这里插入图片描述

  4. 将脚本挂在到场景中(任意游戏物体上)

  5. 通过在“ Installers”属性的检查器中添加新行(按+按钮),然后将TestInstaller拖到其中,将对TestInstaller的引用添加到SceneContext的属性中 在这里插入图片描述

  6. 打开TestInstaller并将上面的代码粘贴到其中

  7. 通过 Edit->Zenject->Validate Current Scenes 验证(非必须步骤)在这里插入图片描述在这里插入图片描述

  8. 运行场景

  9. 另请注意,您可以使用快捷方式CTRL+SHIFT+R“先验证然后运行”。验证通常足够快,这样就不会在运行游戏时增加明显的开销,并且当它确实检测到错误时,由于避免了启动时间,因此迭代速度要快得多。

  10. 观察统一控制台的输出

SceneContext MonoBehaviour是应用程序的入口点,在启动场景之前,Zenject会在其中设置所有依赖项。要将内容添加到Zenject场景中,您需要编写在Zenject中称为“Installer”的文件,该文件声明所有依赖项及其相互之间的关系。在运行installers 后,会自动创建所有标记为“ NonLazy”的依赖项,这就是我们在启动时创建上面添加的Greeter类的原因。

Injection

有四种方式将依赖项注入到您的类中,下面将逐一讲解

构造函数注入

和上面讲解依赖注入时的示例相似。

public class Foo
{
    IBar _bar;

    public Foo(IBar bar)
    {
        _bar = bar;
    }
}

成员变量注入

在Constructor调用之后,随即进行成员变量注入,所有被 [Inject] 特性标注的成员变量在Container中进行匹配搜索并注入值(注:这些成员变量可以被声明为private或者public,不影响注入效果)

public class Foo
{
    [Inject]
    IBar _bar;
}

属性注入

属性注入的工作原理与成员变量注入相同,只是应用于C#属性。就像成员变量一样,setter 可以是 private 的也可以是 public 的。

public class Foo
{
    [Inject]
    public IBar Bar
    {
        get;
        private set;
    }
}

方法注入

方法注入和构造函数注入十分相似。

注意事项:
  • 对于继承自Monobehaviour的类推荐使用方法注入,因为这些类没有构造函数
  • 方法注入是没有数量限制的。在这种情况下,将按基类到派生类的顺序调用它们。这对于避免需要通过构造函数参数将许多依赖关系从派生类转发到基类很有用,同时还可以确保基类的注入方法首先完成,就像构造函数的工作方式一样。
  • 方法注入在其他注入方式完成后调用。通过这种设计,这些方法可以用于执行逻辑初始化,逻辑初始化时可以利用已经完成注入的字段或属性。还要注意,如果只想执行一些初始化逻辑,则可以将参数列表留空。
  • 您可以放心地认为,通过方法注入接收到的依赖项本身已经完成注入了(唯一的例外是具有循环依赖项的情况)。如果您使用方法注入的同时在方法内执行一些基本的初始化,您可能还需要初始化给定的依赖项。
  • 但是请注意,将注入方法用于逻辑初始化可能并不是一个好主意。最好继承IInitializable接口,实现Initialize()方法或者在Start()方法中完成逻辑初始化,因为此时已将完成了整个Object Graph的初始化。
建议:

在使用时更推荐使用构造函数注入和方法注入,成员变量注入和属性注入次之。

  • 构造函数注入会在类的实例创建时强制将依赖项Resolve一次且仅Resolve一次,这通常时我们想要的。在大多数情况下,我们并不想将初始依赖项设置为public属性,因为public属性表明它可以被外部更改。
  • 构造函数注入保证类之间没有循环依赖关系。但是,在使用其他类型的注入时,例如方法/成员变量/属性注入,Zenject确实允许循环依赖
  • 当决定在没有DI框架(例如Zenject)的情况下重用代码的情况,构造函数/方法注入更易于移植。可以对public属性执行相同的操作,但是它更容易出错(容易忘记初始化某个字段 从而导致对象保持无效状态)
  • 最后,构造函数/方法注入使其他程序员在读取代码时可以清楚地看到一个类的所有依赖关系。他们可以简单地查看方法的参数列表。当类具有过多的依赖关系并因此应该拆分时,它会更加明显。(因为其构造函数的参数列表将长(chang)到你无法接受)。
public class Foo
{
    IBar _bar;
    Qux _qux;

    [Inject]
    public Init(IBar bar, Qux qux)
    {
        _bar = bar;
        _qux = qux;
    }
}

Binding

Binding

每个依赖注入框架的终极目的都是将一个类型绑定到实例上。

在Zenject中,依赖关系映射是通过添加绑定到名为Container的对象来完成的。 然后,Container应通过递归解析给定对象的所有依赖关系,来明确如何在应用程序中创建所有对象实例。

当要求Container构造给定类型的实例时,它将使用C#反射查找构造函数参数的列表以及所有标记[Inject]特性的成员变量/属性。 然后,它尝试解析这些依赖关系,根据依赖关系调用构造函数并创建新实例。

因此,每个Zenject应用程序必须告诉Container如何解析这些依赖关系,这是通过Bind命令完成的。 例如,给定以下class:

public class Foo
{
    IBar _bar;

    public Foo(IBar bar)
    {
        _bar = bar;
    }
}

可以使用以下方法绑定Foo类的依赖项:

Container.Bind<Foo>().AsSingle();
Container.Bind<IBar>().To<Bar>().AsSingle();

这告诉Zenject,每个需要Foo类型的依赖项的类都应使用相同的实例(类似于单例模式),它将在需要时自动创建。 同样,任何需要IBar接口的类(如Foo)都将获得相同的Bar类型实例。
bind命令的完整格式如下。 但是请注意,在大多数情况下,不会使用所有这些方法,并且在未指定时它们都具有默认值。

Container.Bind<ContractType>()
    .WithId(Identifier)
    .To<ResultType>()
    .FromConstructionMethod()
    .AsScope()
    .WithArguments(Arguments)
    .OnInstantiated(InstantiatedCallback)
    .When(Condition)
    .(Copy|Move)Into(All|Direct)SubContainers()
    .NonLazy()
    .IfNotBound();

下面将对每个属性进行详细的讲解:

  1. ContractType : 要为其创建绑定的类 该值对应于要注入的字段/参数的类型。

  2. ResultType :绑定的类型 默认值时ContractType,即将ContractType绑定到ResultType的实例上。ResultType必须等于ContractType或从ContractType的派生。 如果未指定,则假定为ToSelf(),这意味着ResultType将与ContractType相同。

  3. Identifier : 用于唯一标识绑定的值。 在大多数情况下,这可以忽略,但是当一个ContractType存在多个绑定的情况下,这可以用来区分不同的实例。

  4. ConstructionMethod :创建/检索ResultType实例的方法。 默认值时FromNew() Examples:FromGetter, FromMethod, FromResolve, FromComponentInNewPrefab, FromSubContainerResolve, FromInstance, etc

  5. Scope =该值确定生成的实例在多次注入中被重用的频率。默认值是 AsTransient,但并不是所有的绑定都有默认值,因此当没有提供默认值时,会抛出异常。通过ConstructionMethod时不需要显式的设定范围(eg:FromMethod, FromComponentX, FromResolve …)
    可以设定为以下状态:
    AsTransient :完全不会重用该实例,每次请求ContractType类时,都会创建新的实例(调用给定的构造方法)
    AsCached :每次请求ContractType时,都会返回ResultType的相同实例,该实例在第一次使用时会懒汉式生成。
    AsSingle :与AsCached完全相同,不同之处在于,如果已经存在ResultType的绑定,有时它会抛出异常。 它确保给定的ResultType在Container内是唯一的。 但是请注意,这只能保证在给定Container中只有一个实例,sub_Container中使用具有相同绑定的AsSingle可以生成第二个实例。
    在大多数情况下,可能只想使用AsSingle,但是AsTransient和AsCached也有其用途。

  6. Arguments :构造ResultType类型的新实例时要使用的参数列表。用于构造函数带参数的情况。

7.InstantiatedCallback :在一些情况下,完成Instantiated后能够自定义对象(对实例化的对象进行部分修改或赋值)十分重要。特别是使用第三方库时,可能有必要在其中一种类型上更改一些字段。 对于这些情况,您可以将方法传递给OnInstantiated,该方法可以自定义新创建的实例。例如:

Container.Bind<Foo>().AsSingle().OnInstantiated<Foo>(OnFooInstantiated);
void OnFooInstantiated(InjectContext context, Foo foo)
{
    foo.Qux = "asdf";
}

同时支持lamda表达式,可以写成:

Container.Bind<Foo>().AsSingle().OnInstantiated<Foo>((ctx, foo) => foo.Bar = "qux");

请注意,您还可以使用FromFactory绑定自定义工厂,该工厂直接调用Container.InstantiateX,然后再对其进行自定义以达到相同效果,但是在某些情况下OnInstantiated会更方便实现。

  1. Condition :条件判定,满足条件时才绑定成功。

  2. (Copy|Move)Into(All|Direct)SubContainers:用到的可能性极低,99%用户可忽略。它可用于自动使绑定由子容器继承。 例如,如果您有一个Foo类,并且希望将Foo的唯一实例自动放置在容器和每个子容器中,则可以添加以下绑定:

Container.Bind<Foo>().AsSingle().CopyIntoAllSubContainers()

换句话说,结果等同于将 Container.Bind ().AsSingle() 语句复制并粘贴到每个子容器的 installer 中。
如果您只想在子容器中使用Foo,而不在当前容器中使用:

Container.Bind<Foo>().AsSingle().MoveIntoAllSubContainers()

如果您只想让Foo注册进子容器而不是这些子容器的子容器,请执行以下操作:

Container.Bind<Foo>().AsSingle().MoveIntoDirectSubContainers()
  1. NonLazy :饿汉式创建,Lazy意味着在使用的时候才创建实例,NonLazy则在完成绑定时就创建实例。

  2. IfNotBound :尝试将其添加到绑定中,如果对应的( contract type + identifier)已经存在,则该绑定跳过。

Construction Methods

  1. FromNew :通过C#中 new 运算符创建。 如果未指定构造方法,则为默认设置。
// 两种写法实现的结果相同
Container.Bind<Foo>();
Container.Bind<Foo>().FromNew();
  1. FromInstance :将给定的实例添加到容器
Container.Bind<Foo>().FromInstance(new Foo());

// 简写,它是从参数类型中推断ContractType
Container.BindInstance(new Foo());

// 通常用于原始类型(c#自带的类型)
Container.BindInstance(5.13f);
Container.BindInstance("foo");

// 批量绑定
Container.BindInstances(5.13f, "foo", new Foo());
  1. FromMethod :通过自定义方法创建实例
Container.Bind<Foo>().FromMethod(SomeMethod);

Foo SomeMethod(InjectContext context)
{
    ...
    return new Foo();
}
  1. FromMethodMultiple :除了允许一次返回多个实例(或返回零个),与FromMethod相同。
Container.Bind<Foo>().FromMethodMultiple(GetFoos);

IEnumerable<Foo> GetFoos(InjectContext context)
{
    ...
    return new Foo[]
    {
        new Foo(),
        new Foo(),
        new Foo(),
    }
}
  1. FromFactory :使用自定义工厂类创建实例。 此构造方法与FromMethod相似,但是在逻辑更复杂或需要依赖项时(因为工厂本身可以注入依赖项)思维更清晰。
class FooFactory : IFactory<Foo>
{
    public Foo Create()
    {
        // ...
        return new Foo();
    }
}

Container.Bind<Foo>().FromFactory<FooFactory>()
  1. FromIFactory :使用自定义工厂类创建实例。 这是比FromFactory更通用,更强大的替代方法,因为可以为自定义工厂使用任何一种构造方法(不同于FromFactory假定的FromNew().AsCached())
    例如,您可以使用如下这样的可编写脚本对象的工厂:
class FooFactory : ScriptableObject, IFactory<Foo>
{
    public Foo Create()
    {
        // ...
        return new Foo();
    }
}

Container.Bind<Foo>().FromIFactory(x => x.To<FooFactory>().FromScriptableObjectResource("FooFactory")).AsSingle();

或者,您可能希望将自定义工厂放置在这样的子容器中:

public class FooFactory : IFactory<Foo>
{
    public Foo Create()
    {
        return new Foo();
    }
}

public override void InstallBindings()
{
    Container.Bind<Foo>().FromIFactory(x => x.To<FooFactory>().FromSubContainerResolve().ByMethod(InstallFooFactory)).AsSingle();
}

void InstallFooFactory(DiContainer subContainer)
{
    subContainer.Bind<FooFactory>().AsSingle();
}

另请注意,以下两行是等效的:

Container.Bind<Foo>().FromFactory<FooFactory>().AsSingle();
Container.Bind<Foo>().FromIFactory(x => x.To<FooFactory>().AsCached()).AsSingle();
  1. FromComponentInNewPrefab :初始化给定的Prefab作为一个新的游戏物体,查找上边的组件,并完成绑定,查询的工作方式类似于unity中GetComponentInChildren,查询自身及其所有子物体上的对应组件,并绑定第一个查找到的组件。
Container.Bind<Foo>().FromComponentInNewPrefab(somePrefab);

在这种情况下,ResultType必须是接口或派生自UnityEngine.MonoBehaviour / UnityEngine.Component
请注意,如果预制件上有多个ResultType匹配项,则它将仅与遇到的第一个匹配,就像GetComponentInChildren的工作方式一样。 因此,如果要绑定预制件,而没有要绑定的特定MonoBehaviour / Component,则只需选择“Transform ”即可,它会与Prefab的Root匹配。

  1. FromComponentsInNewPrefab :与FromComponentInNewPrefab相同,除了将匹配多个值或零值。 例如,您可以使用它,然后在某个位置注入List 。 可以认为类似于GetComponentsInChildren。将同一个Prefab上的多个ContractType绑定成List 。

  2. FromComponentInNewPrefabResource :将给定的预制件(位于给定的资源路径处)实例化为新的游戏对象。与FromComponentInNewPrefab类似,只不过提供的参数是资源的路径。

Container.Bind<Foo>().FromComponentInNewPrefabResource("Some/Path/Foo");
  1. FromComponentsInNewPrefabResource :类似于FromComponentsInNewPrefab ,提供路径参数。
  2. FromNewComponentOnNewGameObject :创建一个新的EmptyGameobject对象,然后在其上实例化给定类型的新组件。(注:ResultType 必须继承自 UnityEngine.MonoBehaviour / UnityEngine.Component)
Container.Bind<Foo>().FromNewComponentOnNewGameObject();
  1. FromNewComponentOnNewPrefab :将传入的预制实例化为新的游戏对象,并在新的游戏对象的根目录上实例化给定组件的新实例。(注:ResultType 必须继承自 UnityEngine.MonoBehaviour / UnityEngine.Component)
Container.Bind<Foo>().FromNewComponentOnNewPrefab(somePrefab);

测试代码:

public class TestAPIInstaller : MonoInstaller
{
    public GameObject prefab;
    public override void InstallBindings()
    {
        Container.Bind<TestComponent>().FromComponentInNewPrefab(prefab).AsTransient().NonLazy();
    }
}

测试结果:
在这里插入图片描述
13. FromNewComponentOnNewPrefabResource :类似于FromNewComponentOnNewPrefab,提供的参数换成路径。

Container.Bind<Foo>().FromNewComponentOnNewPrefabResource("Some/Path/Foo");
  1. FromNewComponentOn :在给定的Gameobject上实例化给定类型的新组件。(笔者理解:相当于给指定的gameobject添加组件并将组件完成绑定)
    ResultType 必须继承自 UnityEngine.MonoBehaviour / UnityEngine.Component
Container.Bind<Foo>().FromNewComponentOn(someGameObject);

TEST代码:

public class TestAPIInstaller : MonoInstaller
{
    public GameObject GO;
    public override void InstallBindings()
    {
        Container.Bind<TestComponent>().FromNewComponentOn(GO).AsSingle().NonLazy();
    }
}

运行前:
在这里插入图片描述
运行后:
在这里插入图片描述
15. FromNewComponentSibling :在当前Transform上实例化给定的新组件。 这里的当前Transform是从注入的对象获取的,因此,该对象必须是MonoBehaviour派生的类型。
注意,如果给定的组件类型已经附加到当前Transform中,则它将仅返回该类型,而不是创建新组件。 结果,此bind语句的功能类似于Unity的[RequireComponent]属性。

Container.Bind<Foo>().FromNewComponentSibling();
  1. FromComponentInHierarchy :在当前Scene的Hierarchy中寻找指定的组件,并将其绑定。 与GetComponentInChildren的工作方式相似,它将返回找到的第一个匹配值。
Container.Bind<Foo>().FromComponentInHierarchy().AsSingle();

TEST:
完成绑定的代码:

public class TestAPIInstaller : MonoInstaller
{
    public override void InstallBindings()
    {
        Container.Bind<TestComponent>().FromComponentInHierarchy().AsSingle();
    }
}

场景中存在的组件:

public class TestComponent : MonoBehaviour
{
    private void Start()
    {
       Debug.Log("当前TestComponent实例的哈希值"+this.GetHashCode());
    }
}

调用DI绑定的组件:

public class TestComponnetA : MonoBehaviour
{
    [Inject] private TestComponent _testComponent;
    private void Start()
    {
        Debug.Log("注入TestComponent实例的哈希值"+_testComponent.GetHashCode());
    }
}

通过对比哈希值,可以确定,Zenject自动在场景中完成了搜索和绑定的任务。
在这里插入图片描述
当使用的context是ScenesContext时(通常情况下使用SceneContext),将搜索整个场景结构(除了sub_Context),换句话说,其功能类似于GameObject.FindObjectsOfType。FromComponentInHierarchy将是一个很大的搜索,应该像在使用GameObject.FindObjectsOfType一样谨慎。
当使用的context是GameObjectContext,仅搜索GameObject根目录和其子目录。(笔者猜测可能这里的Gameobject指的是挂载了GameObjectContext的物体)

  1. FromComponentsInHierarchy :和FromComponentInHierarchy 类似,不过绑定的不是第一个搜索到的组件,而是组件的list列表。在其类可注入List

18.FromComponentSibling :参看FromNewComponentSibling

  1. FromComponentInParents :查找路径为当前Transform和其父物体Transform
  2. FromComponentsInParents :仅搜索位置不同,雷同
  3. FromComponentInChildren :仅搜索位置不同,雷同
  4. FromComponentsInChildren: 仅搜索位置不同,雷同
  5. FromNewComponentOnRoot :仅搜索位置不同,雷同
  6. FromResource :从资源路径加载绑定
Container.Bind<Texture>().WithId("Glass").FromResource("Some/Path/Glass");
  1. FromResources :从资源路径加载绑定列表
  2. FromScriptableObjectResource :从资源路径进行加载脚本,脚本继承自ScriptableObject,在编辑器模式改变此致的参数值会被永久修改,如果不希望这么做,使用FromNewScriptableObjectResource
public class Foo : ScriptableObject
{
}

Container.Bind<Foo>().FromScriptableObjectResource("Some/Path/Foo");
  1. FromNewScriptableObjectResource
  2. FromResolve:
public interface IFoo
{
}

public interface IBar : IFoo
{
}

public class Foo : IBar
{
}

Container.Bind<IFoo>().To<IBar>().FromResolve();
Container.Bind<IBar>().To<Foo>();
//

通过在Container中进行一轮查找来获取实例,换句话说,就是调用 DiContainer.Resolve()
请注意,要使此方法起作用,必须在单独的bind语句中绑定ResultType。 当您要将一个接口绑定到另一个接口时,此构造方法特别有用,如上面的示例所示。
29. FromResolveAll :FromResolve
30. FromResolveGetter

public class Bar
{
}

public class Foo
{
    public Bar GetBar()
    {
        return new Bar();
    }
}

Container.Bind<Foo>();
Container.Bind<Bar>().FromResolveGetter<Foo>(x => x.GetBar());
  1. FromResolveAllGetter
  2. FromSubContainerResolve : 通过在子容器上进行查找来获取ResultType。 请注意,要使此功能起作用,子容器必须具有ResultType的绑定.这种方法非常强大,因为它允许您将相关的依赖项分组到一个Sub_Container中,然后仅公开某些类(也称为“ Facades”)以在更高级别上对该组依赖项进行操作。
  3. FromSubContainerResolveAll

Installers

通常,每个子系统都有一些相关绑定的集合,因此将这些绑定分组为可复用的对象是有意义的. 在Zenject中,此可复用的对象称为“installer”。 您可以如下定义新的installer:

public class FooInstaller : MonoInstaller
{
    public override void InstallBindings()
    {
        Container.Bind<Bar>().AsSingle();
        Container.BindInterfacesTo<Foo>().AsSingle();
        // etc...
    }
}

通过重写InstallBindings方法来添加绑定,这个方法调用了installer被添加进的Context(通常是SceneContext),MonoInstaller可视为MonoBehaviour,因此您可以通过将FooInstaller附加到GameObject来使用。除了此功能外,您可以把他当作一个MonoBehaviour脚本来使用。但是为了触发安装程序,需要添加到SceneContext中进行安装,安装顺序从上到下依次为 scriptable object installers,mono installers,prefab installers,通常顺序并不是很重要,因为在安装过程中不发生实例化。
在这里插入图片描述
通常情况下希望Installer继承自MonoInstaller,这样就可以通过Inspector面板进行设定,除此之外,也有一个名为Installer的简单基类,当你的类不需要继承子Mono时,这将很有作用。

代码:

public class BarInstaller : Installer<BarInstaller>
{
    public override void InstallBindings()
    {
        ...
    }
}

public class FooInstaller : MonoInstaller
{
    public override void InstallBindings()
    {
        BarInstaller.Install(Container);
    }
}

在这种情况下,BarInstaller的类型为Installer <>(注意通用参数),而不是MonoInstaller,这就是为什么我们可以简单地调用BarInstaller.Install(Container)而不要求将BarInstaller添加到场景中的原因。 对BarInstaller.Install的任何调用都会创建一个BarInstaller的temp实例,然后在其上调用InstallBindings。 对于此Installer安装的所有Installers,将重复此操作。 还要注意,在使用Installer <>基类时,我们始终必须将自身作为通用参数传递给Installer <>。 为了使Installer <>基类可以定义静态方法BarInstaller.Install,这是必需的。 它还以这种方式设计来支持运行时参数(如下所述)。
我们使用Installer而不是针对每个场景声明一次所有绑定,主要原因之一是使它们可重复使用。 您可以有针对性的在使用它们的场景中调用FooInstaller.Install。

我们如何在多个场景中重用MonoInstaller?

  1. Sence内的Prefab instances。在将MonoInstaller添加到场景中的Gameobject上之后,可以创建一个Prefab,它允许您跨场景共享在MonoInstaller的 Inspector 面板中完成的所有配置(如果需要,还可以按场景进行覆盖。 将其添加到场景中后,可以将其拖放到Context的Installers属性中。

  2. Prefabs。可以将 installer 的预制体从“Project ”选项卡直接拖到SceneContext的InstallerPrefabs属性中。使用这种方法,无妨像第一种那样按场景进行覆盖,但是可以最大程度避免场景混乱。

  3. 资源文件夹中的Prefabs。可以将installer 预制件放置在Resoures文件夹下,并使用Resources路径直接从代码中安装它们。

除了MonoInstaller和Installer <>之外,另一个选项是使用ScriptableObjectInstaller,它具有一些独特的优势。
从其他installers 调用installers 时,通常要将参数传递给它。

使用 Non-MonoBehaviour的类

ITickable

在某些情况下,最好避免使用MonoBehaviours,而只使用普通的C#类。 Zenject通过提供具有MonoBehaviour某些功能的接口,使您可以更轻松地完成操作。
例如,如果需要每帧运行的代码,则可以实现ITickable接口:

public class Ship : ITickable
{
    public void Tick()
    {
        //每帧执行的代码
    }
}

然后将其绑定:

Container.Bind<ITickable>().To<Ship>().AsSingle();

注:Tick函数调用的顺序可是可以配置的
注:存在ILateTickable和IFixedTickable接口,它们分别对应Unity的LateUpdate和FixedUpdated方法

IInitializable

如果您需要在给定对象上进行一些初始化,可以将初始化代码包含在构造函数中。 但是,这意味着初始化逻辑将出现在正在构造的object graph 中间,因此它可能不是一种好的方式。
更好的替代方法是实现IInitializable,然后在Initialize()方法中执行初始化逻辑,同样需要完成绑定。
在整个object graph 构造完成后,执行Initialize()操作。
注:可以理解为在Awake中调用了构造 object graph 的函数,Start中调用了Initialize(),Awake阶段完成对象的引用设置,在Start阶段完成更加复杂的初始化逻辑。
使用IInitializable与使用构造函数和 [Inject]特性相比,更加合适,因为可以使用与ITickable类似的方式控制初始化执行的顺序。

public class Ship : IInitializable
{
    public void Initialize()
    {
        // 完成初始化逻辑
    }
}

IInitializable非常适合 start_up 初始化,但是对于通过工厂模式动态创建的对象呢?您很可能希望使用[Inject]方法或在创建对象之后调用的显式Initialize方法。 例如:

public class Foo
{
    [Inject]
    IBar _bar;

    [Inject]
    public void Initialize()
    {
        ...
        _bar.DoStuff();
        ...
    }
}

IDisposable

如果您有外部资源要在应用程序关闭、场景更改或出于任何原因破坏context 对象时清理,则可以为您的class实现IDisposable接口,如下所示:

public class Logger : IInitializable, IDisposable
{
    FileStream _outStream;

    public void Initialize()
    {
        _outStream = File.Open("log.txt", FileMode.Open);
    }

    public void Log(string msg)
    {
        _outStream.WriteLine(msg);
    }

    public void Dispose()
    {
        _outStream.Close();
    }
}

绑定:

Container.Bind(typeof(Logger), typeof(IInitializable), typeof(IDisposable)).To<Logger>().AsSingle();

或者使用快捷方式:

Container.BindInterfacesAndSelfTo<Logger>().AsSingle();

实现原理:当场景改变或者Unity应用程序关闭时,所有的MonoBehaviours都会调用unity的内部事件OnDestroy(),当销毁SceneContext时,继承了Disposable的所有对象上触发Dispose()函数

还可以实现ILateDisposable接口,该接口与ILateTickable的工作方式相似,因为它将在触发所有IDisposable对象之后调用该接口。 但是,在大多数情况下,如果命令执行顺序有问题,最好设置一个明确的执行顺序。

BindInterfacesTo 与 BindInterfacesAndSelfTo

如果你在代码中使用了ITickable,IInitializable,IInitializable这些接口,你可能需要完成这样的绑定:

Container.Bind(typeof(Foo), typeof(IInitializable), typeof(IDisposable)).To<Logger>().AsSingle();

这其实是很冗余的写法,尤其是当我们决定Foo不在使用Tick()或Dispose()时,需要重新修改绑定代码。
一好一点的想法是始终使用这些接口,也就不用担心是否有缺漏的情况了,大不了将方法置为空方法:

Container.Bind(new[] { typeof(Foo) }.Concat(typeof(Foo).GetInterfaces())).To<Foo>().AsSingle();

上述的写法仍有些冗余,Zenject提供了一个API来快捷的实现上面的想法,使用后我们可以给Foo类添加或删除接口,与此同时不需要去修改绑定:

Container.BindInterfacesAndSelfTo<Foo>().AsSingle();

问题又来了,当我们只想绑定接口,而将Foo类对其它类隐藏,此时就需要使用BindInterfacesTo方法

Container.BindInterfacesTo<Foo>().AsSingle()

等价于:

Container.Bind(typeof(IInitializable), typeof(IDisposable)).To<Foo>().AsSingle();

使用Unity的 Inspector 面板来配置Settings

将大多数代码编写为普通C#类而不继承MonoBehaviour的一种含义是,您将失去使用Inspector 面板为其配置数据的能力。 但是,您仍然可以通过使用以下模式在Zenject中利用此功能:

public class Foo : ITickable
{
    readonly Settings _settings;

    public Foo(Settings settings)
    {
        _settings = settings;
    }

    public void Tick()
    {
        Debug.Log("Speed: " + _settings.Speed);
    }

    [Serializable]
    public class Settings
    {
        public float Speed;
    }
}

在Installer中完成绑定:

public class TestInstaller : MonoInstaller<TestInstaller>
{
    public Foo.Settings FooSettings;

    public override void InstallBindings()
    {
        Container.BindInstance(FooSettings);
        Container.BindInterfacesTo<Foo>().AsSingle();
    }
}

上面的写法等价于:(笔者推荐使用 WithArguments的方式,因为字面意思,一看就懂)

public class TestInstaller : MonoInstaller<TestInstaller>
{
    public Foo.Settings FooSettings;

    public override void InstallBindings()
    {
        Container.BindInterfacesTo<Foo>().AsSingle().WithArguments(FooSettings);
    }
}

现在,在运行场景时,我们可以直接从Inspector面板中调整参数。

此外,笔者认为使用ScriptableObjectInstaller而不是MonoInstaller会更加便捷,尤其是在游戏调试参数时非常有效,运行时修改的参数可以得已保留,还可以将所有的参数控制调整到一个面板上。

Object Graph 验证

在这里,我们先整理一下使用DI框架的调试工作流。

  1. 在代码中添加绑定
  2. 运行应用
  3. 观察大量与DI有关的异常
  4. 修改绑定解决问题
  5. 重复 第2步----第4步的调试过程

这种调试流程对于小型项目来说毫无问题,但是随着项目复杂性的增加,笔者认为这种工作流的繁琐程度是呈指数增长的。特别是如果您的应用程序的启动时间比较长,或者异常仅在工厂模式的不同阶段,问题会变得更加严重。 最好使用一些工具来分析 object graph 并准确告诉您所有丢失的绑定在发生在哪里,而无需启动整个应用程序。
在unity中点击 Edit -> Zenject -> Validate Current Scene (快捷键:SHIFT+ALT+V )这个命令为当前的Scene执行Installer程序,其运行结构是得到一个完成了绑定的Container,然后遍历object graph,就可以验证是否已经建立了所有的绑定(无需实际实例化任何绑定)。其本质就是执行一个空启动,后台通过将虚拟对象存储在容器中来代替实际实例化类。
执行Edit -> Zenject -> Validate(快捷键:CTRL+SHIFT+R),将验证场景,如果通过,则自动运行游戏场景。
在这里插入图片描述
在这里插入图片描述
注:这还将包括工厂和内存池,这特别有用,因为这些错误可能要等到启动后的某个时间才能被捕获。
使用时两点需要明确:

  1. 验证过程没有执行任何实际的逻辑代码-仅调用 install-bindings 。 这意味着,如果Installer中有绑定程序以外的其他逻辑,则这些逻辑也会被执行,并且在运行验证时可能会引起问题(尤其时这些逻辑需要注入值时)
  2. 验证过程中空值注入到实际实例化的依赖项中(无论绑定了什么)

但是事无绝对,如果你希望在验证模式下注入一些类。 在这种情况下,您可以将它们标记为[ZenjectAllowDuringValidation].

自定义验证

如果您想添加自己的验证逻辑,则只需让您的一个类从IValidatable继承即可。 完成此操作后,只要将您的类绑定到某个installer 中的某个位置,它将在验证期间实例化,并且将调用其Validate()方法。 但是请注意,它具有的所有依赖项都将被注入为null(除非标记为[ZenjectAllowDuringValidation]属性)。
如果不符合你的验证逻辑,可以在Validate方法中抛出异常,或在Console控制台打印日志。在自定义验证中所办的事,核心就是实例化本来不会被验证的类型。通过在验证期间实例化它们,将确保可以解决所有依赖问题。
例如,如果您创建一个使用Container.Instantiate ()实例化类型的自定义工厂,则Foo将不会得到验证,因此直到运行时您都不会发现它是否缺少某些依赖项。 但是,可以通过让工厂实现IValidatable,然后在Validate()方法内调用Container.Instantiate ()来解决此问题。

场景绑定

在许多情况下,您已经在Unity编辑器中将许多MonoBehaviours添加到场景中(在编辑器模式而不是运行模式),并且还希望将这些MonoBehaviours添加到Zenject Container 中,以便可以将它们注入到其他类中。
通常的做法是在installer 中向这些对象添加 public 引用,如下所示:

public class Foo : MonoBehaviour
{
}

public class GameInstaller : MonoInstaller
{
    public Foo foo;

    public override void InstallBindings()
    {
        Container.BindInstance(foo);
        Container.Bind<IInitializable>().To<GameRunner>().AsSingle();
    }
}

public class GameRunner : IInitializable
{
    readonly Foo _foo;

    public GameRunner(Foo foo)
    {
        _foo = foo;
    }

    public void Initialize()
    {
        ...
    }
}

一般来讲,这种方法毫无问题,但是在某些特殊情况,会变得很麻烦。例如,如果你想添加任意数量的敌人到场景中,并且还希望所有的敌人都添加进Contanner中。这种情况,你需要手动的将每一个敌人拖拽到 Installer 的Inspector中,这是一个极易出错的地方,可能发生遗漏,也可能明明已经删除了敌人却忘记删除Inspector中的空引用。
另一种方法是使用FromComponentInHierarchy绑定方法,如下所示:

public class GameInstaller : MonoInstaller
{
    public override void InstallBindings()
    {
        Container.Bind<Foo>().FromComponentInHierarchy().AsTransient();
        Container.Bind<IInitializable>().To<GameRunner>().AsSingle();
    }
}

工作原理:当需要Foo类型的依赖项的时候,zenject会在整个场景中搜索Foo类型的MonoBehaviours。其功能将非常类似于使用Unity的FindObjectsOfType方法。请注意,由于此方法可能是非常繁重的操作,因此可能希望将其标记为AsCached或AsSingle,如下所示:

public class GameInstaller : MonoInstaller
{
    public override void InstallBindings()
    {
        Container.Bind<Foo>().FromComponentInHierarchy().AsCached();
        Container.Bind<IInitializable>().To<GameRunner>().AsSingle();
    }
}

这样,只在第一次注入时搜索,而不是每次注入都执行一遍搜索,减少对性能的影响。在我们期望有多个Foos的情况下,还可以使用FromComponentsInHierarchy(请注意复数s)。
执行此操作的另一种方法是使用ZenjectBinding组件。 您可以通过将ZenjectBinding MonoBehaviour添加到要自动添加到Zenject容器的同一游戏对象中来实现。例如,如果场景中具有类型为Foo的MonoBehaviour,则可以在其旁边添加ZenjectBinding,然后将Foo组件拖到ZenjectBinding组件的Component属性中。
installer 中代码会变为:

public class GameInstaller : MonoInstaller
{
    public override void InstallBindings()
    {
        Container.Bind<IInitializable>().To<GameRunner>().AsSingle();
    }
}

图片中将Transform自动绑定
在这里插入图片描述
详解ZenjectBinding 组件属性:

Bind Type 这将确定要使用的“contract type”。 可以将其设置为以下任何值:self / AllInterfaces / AllInterfacesAndSelf / BaseType
在这里插入图片描述

  1. Self :等同于下列代码
//第一种写法
Container.Bind<Foo>().FromInstance(_foo);
//第二种写法
Container.BindInstance(_foo);

注意:如果复制带有Foo的游戏对象,使场景中具有多个Foo时,他们都将以这种方式绑定到Container,必须将上面的GameRunner更改为List ,否则我们将获得Zenject异常

  1. AllInterfaces :
    相当于下面代码:
Container.BindInterfacesTo(_foo.GetType()).FromInstance(_foo);

但是请注意,在这种情况下,GameRunner必须在其构造函数中要求输入IFoo类型。 如果我们离开GameRunner询问类型为Foo,则Zenject将抛出异常,因为BindInterfaces方法仅绑定接口,而不绑定具体类型。如果还需要具体类型,则可以使用:
3. AllInterfacesAndSelf

//等价于
Container.BindInterfacesAndSelfTo(_foo.GetType()).FromInstance(_foo);
  1. BaseTyp
//等价于
Container.Bind(_foo.GetType().BaseType()).FromInstance(_foo)

Identifier 该值在大多数情况下可以保留为空。 它将确定将什么用作绑定的标识符。 例如,当设置为“ Foo1”时,它等效于执行以下操作:

Container.BindInstance(_foo).WithId("Foo1");

在这里插入图片描述
Use Scene Context标志位,是否使用Scene Context,也可以在Context中赋值Scene Context,不过使标志位毕竟简单一点。
Context :这是可选的,在大多数情况下应保持不变。 这将确定将绑定应用于哪个Context。如果不设置,将使用GameObject所在的Context。在大多数情况下,它将是SceneContext,但是如果它位于GameObjectContext内部,则它将绑定到GameObjectContext容器中。此字段的一个重要用例是,对于组件位于GameObjectContext内部的情况,允许将SceneContext拖到该字段中。 这使您可以将此MonoBehaviour视为GameObjectContext给定的整个子容器的Facade。
在这里插入图片描述

使用准则和技巧

1. 如果要注入对象的依赖项,不要使用GameObject.Instantiate

如果要在运行时实例化预制件并存在MonoBehaviours需要自动注入,强烈建议使用工厂模式。 可以通过调用InstantiatePrefab方法直接使用DiContainer实例化预制件。 与GameObject.Instantiate相比,使用这些方法将确保正确填充所有带有[Inject]属性标记的字段,并调用预制中的所有标记为[Inject]的方法。

2. Best practice with DI is to only reference the container in the composition root "layer"

3.不要对动态创建的对象使用IInitializable,ITickable和IDisposable
IInitializable类型的对象仅初始化一次-在Unity的启动阶段时。 如果通过工厂创建对象,并且该对象派生自IInitializable,则不会调用Initialize()方法。 在这种情况下,您应该使用[Inject]方法,或者在调用Create之后自己显式调用Initialize()。
上述说明同样适用于ITickable和IDisposable。 除非它们是启动时创建的原始 object graph 的一部分,否则从这些操作都不会发生。
如果您动态创建了具有Update()方法的对象,通常最好手动调用这些对象,并且通常会有一个类似高层管理器的类,从中可以做到这一点。 但是,如果您希望将ITickable用于动态对象,则可以声明对TickableManager的依赖关系,也可以显式添加/删除它。

4. 具有多个构造函数的情况
可以有多个构造函数,但是必须使用[Inject]属性标记其中一个构造函数,以便Zenject知道要使用哪个构造函数。 如果您有多个构造函数,但都没有用[Inject]标记,则Zenject会猜测预期的构造函数是最少参数的构造函数。

5.懒汉式实例化对象和 object graph
Zenject不会立即实例化由您在installers中设置绑定的每个对象。 它将仅实例化标记为NonLazy的那些绑定。 所有其他绑定都在需要时实例化。 所有的NonLazy对象及其所有依赖项都构成应用程序的’initial object graph’ 。 请注意,这会自动包括实现IInitializable,ITickable,IDisposable等的所有类型。因此,如果你有一个绑定因为’initial object graph’ 中没有引用,而未进行实例化。则可以通过NonLazy进行修改。

6. Restrict the use of bind commands to the ‘composition root’ only
换句话说,在安装阶段完成后,请勿调用Container.Bind,Container.Rebind或Container.Unbind。 这很重要,因为在安装完成后立即构造了应用程序的initial object graph ,并且需要访问完整的绑定集。

7.遇到执行顺序是错误的情况,例如注入太晚,或者在正确的时间未调用Initialize()事件,等等。
这可能是因为Zenject类ProjectKernel或SceneKernel或SceneContext的“脚本执行顺序”不正确。 这些类应始终具有最早或接近最早的执行顺序。 默认情况下,应该已经完成了设置。(因为此设置包含在这些类的cs.meta文件中)。 但是,如果您自己编译Zenject或具有独特的配置,则可能要确保做到这一点,方法是转到““Edit -> Project Settings -> Script Execution Order””,并确认这些类在顶部,然后再进行操作。

发布了6 篇原创文章 · 获赞 11 · 访问量 3万+

猜你喜欢

转载自blog.csdn.net/weixin_43405845/article/details/104344019
今日推荐