Zenject BindのAsSingle, AsCached, AsTransientの違い

Zenjectを使い始めて、最初の方に?ってなるのがAsSingle(), AsCached(), AsTransient()な気がします。 正直ZenjectのREADMEを何度読んでもイマイチよくわからない。 いろいろ挙動を調べたので解説します。

Scopeとは

AsSingle(), AsCached(), AsTransient()はZenjectでScopeと呼ばれるものです。 READMEによると「生成されたインスタンスが再利用される頻度」みたいなことが書いてあります。 日本語もしくは英語でOKな説明ですが、Scopeは対象のインスタンスがDIコンテナにて要求されたときに、 新しく生成するのか、同じものを使いまわしてInjectするのかを指定するメソッドになります。

ひとまず、Scopeの指定を何もせずに何かをBindしてみると...

public class SceneInstaller : MonoInstaller<SceneInstaller>
{
    public override void InstallBindings()
    {
        Container.Bind<Sample>();
    }
}

エラーになります。

ZenjectException: Assert hit! Scope must be set for the previous binding!  Please either specify AsTransient, AsCached, or AsSingle. Last binding: Contract: Sample, Identifier: NULL 

READMEではDefault: AsTransientみたいなことが書いてありますが、 何かしら指定しないといけないようです。

使い方は以下みたいなかんじです。

public class SceneInstaller : MonoInstaller<SceneInstaller>
{
    public override void InstallBindings()
    {
        Container.Bind<Sample>().AsSingle();
    }
}

シンプルな場合のAsSingle, AsCached, AsTransientの違い

まず、前置きとして、 同じコンテナに同じタイプのインスタンスは、そのままでは複数個バインドできません。 これはScopeをつけても同じです。

つまり、以下はいずれもエラーになります。

public class SceneInstaller : MonoInstaller<SceneInstaller>
{
    public override void InstallBindings()
    {
        Container.Bind<Sample>().AsSingle();
        Container.Bind<Sample>().AsSingle(); // エラー
    }
}

public class SceneInstaller : MonoInstaller<SceneInstaller>
{
    public override void InstallBindings()
    {
        Container.Bind<Sample>().AsTransient();
        Container.Bind<Sample>().AsTransient();  // エラー
    }
}

public class SceneInstaller : MonoInstaller<SceneInstaller>
{
    public override void InstallBindings()
    {
        Container.Bind<Sample>().AsCached();
        Container.Bind<Sample>().AsCached();  // エラー
    }
}

それを踏まえて三者の違いを比較します。

AsSingle

AsSingleはコンテナ内でインスタンスが1つであることを保証するように動作します。 複数のオブジェクトからInjectの要求があったときに、必ず同じインスタンスが注入されます。

f:id:miki05:20180814222232p:plain

AsCached

AsCachedはもし利用できるインスタンスがコンテナ内に存在すれば、 そのインスタンスをInject時に利用します。 ぶっちゃけ、このシンプルな例ではAsSingleとほぼ等価です。

f:id:miki05:20180814222223p:plain

AsTransient

AsTransientはインスタンスの再利用をまったく行いません。 Injectの要求があるたびに、コンストラクションのフローが走り、 それぞれのクラスに、別々のインスタンスが注入されます。

f:id:miki05:20180814222211p:plain

AsSingleとAsCachedのちがい

正直、AsSingleとAsTransientはわかりやすいです。 AsSingleはSingletonのように1つしか存在しない、AsTransientは毎回別物が生成されるといった覚え方で大丈夫そう。

ではAsCachedとAsSingleのちがいは...?

実はいくつかのケースで違いがあります。 その一つがIdを指定するケースです。 Zenjectでは以下のようにWithIdメソッドでBindするインスタンスにIdをつけ、Inject時にそのIdを指定できます。

using UnityEngine;
using Zenject;

public class SceneInstaller : MonoInstaller<SceneInstaller>
{
    public override void InstallBindings()
    {
        Container.Bind<Sample>().AsCached();
        Container.Bind<Sample>().WithId(1).AsCached();
        Container.Bind<Sample>().WithId(2).AsCached();
    }
}

class ClassA 
{
    [Inject(Id = 1)] Sample sample;
}

つまり、Idをつけ識別可能な状態であれば、 AsCachedは複数のBindの指定が可能で、別々のインスタンスを生成します。

f:id:miki05:20180814222155p:plain

逆にAsSingleはIdをつけようが何をしようがエラーになります。

f:id:miki05:20180814222119p:plain

ちなみにAsTransientでもIdをつけられますが、 インスタンスの再利用はされないため、あんまり意味がないです。 (WithArgumentsを使う場合は意味あるかも)

Sub Container(Context間で親子関係)がある場合

Scopeはコンテナに対しての指定です。 コンテナが異なれば、例え親のコンテナに同じオブジェクトがすでにバインドされていても再度バインドできます。

この場合、インスタンスが要求された際、 同じコンテナ内のインスタンスが優先して解決されます。

f:id:miki05:20180814222109p:plain

また、Idを指定することで親のコンテナの中のインスタンスを指定することもできます。

f:id:miki05:20180814222100p:plain

つまりAsSingleを指定した場合であったとしても、 コンテナごとに複数の同じクラスのインスタンスが生成されるということになります。 ここがSingletonとは違うところですね。

ちなみに親と子でIdが重複したときの挙動ですが、 Bindは可能で自身のコンテナ内のインスタンスを優先している感じです。

InterfaceをBindする場合

InterfaceをBindする場合、実は以下のような記法もエラーになりません。

public class SceneInstaller : MonoInstaller<SceneInstaller>
{
    public override void InstallBindings()
    {
        Container.Bind<Sample>().AsCached();
        Container.Bind<ISample>().To<Sample>().AsCached();
    }
}

その場合、SampleをInjectした方と、ISampleをInjectした方で、違うインスタンスが解決されます。 (おそらくBindInterfacesAndSelfToを使ったほうが予期した動作になりそう)

ちなみにAsSingleを利用したときの以下はエラーになります。(前のバージョンではできた?)

public class SceneInstaller : MonoInstaller<SceneInstaller>
{
    public override void InstallBindings()
    {
        Container.Bind<Sample>().AsSingle();
        Container.Bind<ISample>().To<Sample>().AsSingle();
    }
}

Unbindした場合

Zenject的にはUnbindはあんまりよくない使い方らしいですが、以下のような挙動になります。

  • 同じクラスのAsSingleによるUnbind、再Bindはエラーになる。
public class SceneInstaller : MonoInstaller<SceneInstaller>
{
    public override void InstallBindings()
    {
        Container.Bind<Sample>().AsSingle();
        Container.Unbind<Sample>();
        Container.Bind<Sample>().AsSingle();
    }
}
  • 同じクラスのAsCachedによるUnbind、再Bindはできる。
public class SceneInstaller : MonoInstaller<SceneInstaller>
{
    public override void InstallBindings()
    {
        Container.Bind<Sample>().AsCached();
        Container.Unbind<Sample>();
        Container.Bind<Sample>().AsCached();
    }
}
  • AsCachedをAsSingleで再Bindするとエラーになる
public class SceneInstaller : MonoInstaller<SceneInstaller>
{
    public override void InstallBindings()
    {
        Container.Bind<Sample>().AsCached();
        Container.Unbind<Sample>();
        Container.Bind<Sample>().AsSingle();
    }
}

まとめ

ZenejctのScopeについて解説しました。

  • AsSingleはコンテナ内でなんだかんだ1つのインスタンスになる
  • AsTransientは要求されるたびに毎回インスタンシングフローが走る
  • AsCachedはできる限りインスタンスを使いましてWithIdとかを利用すると識別とかができる
  • AsSingleでもContainerの親子関係を作って複数インスタンスを使いわけることもできる

ただ、Idを使いまくったり、Unbindを使いまくったたり、Interfaceを変な風に使いまくると 多分わけわかんなくなるのでやめたほうが良さげなきがします。