imog

主にUnityとかの事を書いています

InjectとInjectLocal attribute

Zenjectのソース読んでたらInjectLocalというAttributeが内部にあったのでなんだろうと調べたメモ。実用的なやつではない。

どのContainerからInjectされるのか

そもそも、Bindされたオブジェクトはどこから依存しているオブジェクトをもらえるのでしょう。これは基本的には自身がBindされているContainerになります。次のコードを見てみましょう。Fooに依存しているBarを生成するコードです。

public class Foo {
    public ContainerName { get; private set; }
    public Foo(string containerName) {
        ContainerName = containerName;
    }
}
public class Bar { 
    [Inject] Foo _foo;
    public void DoBar() => Debug.Log(_foo.ContainerName);
}
public class FooInstaller : MonoInstaller
{
    public override void InstallBindings()
    {
        // 現在のContainerとProjectContextの両方でFooを用意しておく
        ProjectContext.Instance.Container.Bind<Foo>().AsCached().WithArguments("ProjectContext");
        Container.Bind<Foo>().AsCached().WithArguments("Scene");
        var subContainer = Container.CreateSubContainer();
        subContainer.Bind<Foo>().AsCached().WithArguments("Sub");

        // 現在のContainerにBarをBindする
        Container.Bind<Bar>();
        Container.Resolve<Bar>().DoBar();
    }
}
=> Scene

この場合、Barに渡されるFooインスタンスは同じContainerに所属しているインスタンスを渡されます。 同じContainerにFooが存在しない場合、親のContainerを探しに行きます。親も持っていなかったらさらにその親・・と最終的にProjectContextまで辿っていくことになります。逆に自身のSubContainerを見に行くなど下っていくことはありません。

上を辿っていくのが基本的なルールとして、どのくらいまで辿っていくのかという部分はAttributeやOptionで制御できたりします。

InjectLocal

InjectLocalをつけると親を辿りません。

public class Bar { 
    // 同じContainerからしか探さない
    [InjectLocal] Foo _foo;
    public void DoBar() => Debug.Log(_foo.ContainerName);
}
public class FooInstaller : MonoInstaller
{
    public override void InstallBindings()
    {
        // 現在のContainerとProjectContextの両方でFooを用意しておく
        ProjectContext.Instance.Container.Bind<Foo>().AsCached().WithArguments("ProjectContext");
        // BarがいるContainerにはFooをBindしない
        //Container.Bind<Foo>().AsCached().WithArguments("Scene");
        var subContainer = Container.CreateSubContainer();
        subContainer.Bind<Foo>().AsCached().WithArguments("Sub");

        // 現在のContainerにBarをBindする
        Container.Bind<Bar>();
        Container.Resolve<Bar>().DoBar();
    }
}
=> ZenjectException: Unable to resolve 'Foo' while building object with type 'Bar'. Object graph:
Bar

自身の所属するContainerにはないのでそこでエラーが出てしまいます。

こんな感じで実は細かい設定ができたりします。Attributeの書き換えでもいいですし、Inject(Source = InjectSources.Local)といったInjectSources列挙体で渡すやりかたでも同じことができます。列挙体は以下の四つです。

  • Any
  • Local
  • Parent
  • AnyParent

InjectSources.Any

自身から探して、見つからなければ親を延々と辿っていきます。

通常のInjectがAnyに設定されているのでいつものやつです。

InjectSources.Local

上記の通り、自身のContainerだけを探して見つからなければエラーを吐きます。 InjectLocalの設定と同じです。

InjectSources.Parent

自身の直接の親のContainerを探しに行きます。自身と同じContainerに存在していても参照されません。親が持っていない場合エラーを吐きます。

InjectSources.AnyParent

自身の親を延々と辿っていきます。自身のContainerは参照しません。

参考

InjectSourcesのテストコード見るとなるほど感あります。

Zenject/TestInjectSources.cs at 0ea9690c9ec8ec8edeff7a65a202f7220ace839d · svermeulen/Zenject · GitHub

ちなみにZenject内部ではKernel内部の各種Managerが混ざらないようにInjectLocalで分けてるっぽい。

サブコンテナ使ったりProjectContextでBindしてるインスタンスと同一のものが必要になったりとかしたら明示的に分けていいかもしれませんね。スコープが狭まることはいいこと。