LoginSignup
29
22

More than 5 years have passed since last update.

【Unity】new DiContainer()から理解するZenject【DI】

Last updated at Posted at 2018-12-21

はじめに

アドベントカレンダー初参加なので張り切ってたくさん書きました。
みなさんのZenjectの理解の助けになれば幸いです。

本記事について

Zenject の存在意義や利用価値については、すでにためになる解説記事がたくさんあります。

しかし、Installerの正しい書き方にフォーカスした記事を見たことがないので、本記事ではさらにその前段階として、DiContainerのドキュメントに載っていない挙動を細かく調べます。

DiContainerというのはZenjectによるDIの根幹を担っている重要なクラスで、Zenjectを使ったことがある人ならInstallerInstallBindings()メソッド内でContainerという名前で触っているはずです。

本記事ではまずnew DiContainer()で空のDiContainerを手にするところから始めて、いろいろ試しながらその挙動を明らかにしていきます。

この記事を読んでペアレンティング周りやコンストラクタ引数の解決の挙動が理解できれば、「Installer書いたけど謎のエラーが出て直し方わからん」ということは起きにくくなると思います🎉

 
ちなみにほぼ全てのケースで登場するResolve()ですが、主にZenject内部で呼ばれるメソッドなのでZenject使う側が呼び出すことはほとんどないです。

対象読者

スクリーンショット 2018-12-19 21.37.23.png

解説するコードについて

Test Runnderで動かせるテストコードの形でたくさん書いて調べたので、そのテストコードを貼っていきます。必要に応じて短い解説/まとめを入れます。

コード自体はGitHubに上げてあるので、cloneして実際に動かしてみてください。

git clone https://github.com/su10/Zenject-Usage.git

 
「Test Runnerてなんぞ?」「テスト書いたこと無い」「テストコード読めない」な人向け↓

動作環境

  • Unity: 2018.2.1f1
  • Zenject: 7.3.1

下準備

テストコードの記述量を減らすために以下のクラスを用意しました。

  • ContainerDiContainerクラスのインスタンス(テスト実行ごとに初期化)
  • AssertThrows()Assert.Throws()のラッパーで、エラーの内容をコンソールに表示
DiContainerTestBase.cs
using System;
using NUnit.Framework;
using UnityEngine;
using Zenject;

public abstract class DiContainerTestBase
{
    protected DiContainer Container;

    // テスト毎に新しくコンテナを作り直す
    [SetUp]
    public virtual void SetUp()
    {
        Container = new DiContainer();
    }

    protected void AssertThrows<T>(Action action) where T : Exception
    {
        Assert.Throws<T>(() =>
        {
            try
            {
                action.Invoke();
            }
            catch (T e)
            {
                // エラーの内容を出力
                if (e.InnerException != null)
                {
                    Debug.Log(e.InnerException.Message);
                }
                else
                {
                    Debug.Log(e.Message);
                }

                throw;
            }
        });
    }
}

以下ではテストクラスごとに上記のクラスを継承して使っていきます。

DiContainerの挙動確認

1.バインドとアンバインド

次のようなクラスを用意しました。このクラスにテストとして知りたい挙動を記述していきます。

_1_BindInstanceTest.cs
using NUnit.Framework;
using Zenject;

public class _1_BindInstanceTest : DiContainerTestBase
{
    // ここにテストコードを書いていく
}

全文(GitHub)

プリミティブ型のインスタンスをバインド

    [Test]
    public void _01_BindPrimitive()
    {
        // いろんな型のインスタンスをバインド
        Container.Bind<int>().FromInstance(1);
        Container.Bind<float>().FromInstance(2f);
        Container.Bind<double>().FromInstance(3d);
        // BindInstance<T>()はBind<T>().FromInstance<T>()のショートハンド
        Container.BindInstance(4m);
        Container.BindInstance("str");
        Container.BindInstance(true);

        // Resolve()でバインドしたインスタンスを返す(何度呼んでもエラーにならない)
        for (var i = 0; i < 10; i++)
        {
            Assert.AreEqual(1, Container.Resolve<int>());
            Assert.AreEqual(2f, Container.Resolve<float>());
            Assert.AreEqual(3d, Container.Resolve<double>());
            Assert.AreEqual(4m, Container.Resolve<decimal>());
            Assert.AreEqual("str", Container.Resolve<string>());
            Assert.AreEqual(true, Container.Resolve<bool>());
        }
    }

同じ型のインスタンスを複数バインド

    [Test]
    public void _02_BindMultiple()
    {
        // 同じ型のインスタンスを複数バインド
        Container.BindInstance("a");
        Container.BindInstance("b");
        Container.BindInstance("c");
        // BindInstances()でまとめてバインドできる
        Container.BindInstances("d", "e", "f");

        // Resolve<T>()はTが複数バインドされているとエラー
        AssertThrows<ZenjectException>(() => Container.Resolve<string>());
        // -> Found multiple matches when only one was expected for type 'string'.

        // ResolveAll<T>()はバインドした全てのTインスタンスをList<T>に詰めて返す
        var list = Container.ResolveAll<string>();
        Assert.AreEqual(6, list.Count);
        Assert.AreEqual("a", list[0]);
        Assert.AreEqual("b", list[1]);
        Assert.AreEqual("c", list[2]);
        Assert.AreEqual("d", list[3]);
        Assert.AreEqual("e", list[4]);
        Assert.AreEqual("f", list[5]);

        // BindInstances()は違う型のインスタンスもまとめてバインド可能
        Container.BindInstances(1, 2f, true);
        Assert.AreEqual(1, Container.Resolve<int>());
        Assert.AreEqual(2f, Container.Resolve<float>());
        Assert.AreEqual(true, Container.Resolve<bool>());
    }

対応する型がバインドされていないときの解決

    [Test]
    public void _03_ResolveWhenNoBindings()
    {
        // Resolve<T>()はTがバインドされていないとエラー
        AssertThrows<ZenjectException>(() => Container.Resolve<string>());
        // -> Unable to resolve 'string'.

        // 何もバインドしていない状態のResolveAll<T>()は空のList<T>を返す(エラーにはならない)
        Assert.AreEqual(0, Container.ResolveAll<string>().Count);

        // ResolveId(),ResolveIdAll()も同じ
        AssertThrows<ZenjectException>(() => Container.ResolveId<string>("foo"));
        // -> Unable to resolve 'string (ID: foo)'.
        Assert.AreEqual(0, Container.ResolveIdAll<string>("foo").Count);
    }

ResolveId()ResolveIdAll()については次参照。

idつきのバインド

    [Test]
    public void _04_BindMultipleWithId()
    {
        // 同じ型のインスタンスをidつきで複数バインド
        Container.BindInstance("a").WithId("first");
        Container.BindInstance("b").WithId("second");

        // ResolveId()でidを指定して解決できる
        Assert.AreEqual("a", Container.ResolveId<string>("first"));
        Assert.AreEqual("b", Container.ResolveId<string>("second"));

        // Resolve()による解決ではidつきのバインドは無視される
        // この場合はidなしでマッチするバインドが存在しないので"Found multiple matches"のエラーではなく解決不可エラー
        AssertThrows<ZenjectException>(() => Container.Resolve<string>());
        // -> Unable to resolve 'string'.

        // ResolveAll()による解決も同様、idつきのバインドは無視される
        Assert.AreEqual(0, Container.ResolveAll<string>().Count);

        // 一つだけidなしでバインドするとResolve()で解決できる
        Container.BindInstance("c");
        Assert.AreEqual("c", Container.Resolve<string>());
    }

WithId(id)でidつきバインドすると、型のみでバインドしたときとは違う型でバインドしたような挙動になります。

ResolveIdAll()による解決

    [Test]
    public void _05_ResolveIdAll()
    {
        // idの指定はobject可(stringである必要はない)
        var id = new object();
        Container.BindInstance("a").WithId(id);
        Container.BindInstance("b");

        // ResolveIdAll<T>()はT&&idでマッチしたインスタンスをList<T>で返す
        var listWithId = Container.ResolveIdAll<string>(id);
        Assert.AreEqual(1, listWithId.Count);
        Assert.AreEqual("a", listWithId[0]);

        // 同じ型・同じidでバインドしてもエラーにならず、全てResolveIdAll()で取得できる
        Container.BindInstance("A").WithId(id);
        listWithId = Container.ResolveIdAll<string>(id);
        Assert.AreEqual(2, listWithId.Count);
        Assert.AreEqual("a", listWithId[0]);
        Assert.AreEqual("A", listWithId[1]);
    }

Unbind()

    [Test]
    public void _06_Unbind()
    {
        // バインド
        Container.BindInstance("str");
        Assert.AreEqual("str", Container.Resolve<string>());

        // Unbind()でバインド前の状態に戻る(Resolve()がエラーになる)
        Container.Unbind<string>();
        AssertThrows<ZenjectException>(() => Container.Resolve<string>());
        // -> Unable to resolve 'string'.

        // 複数バインド->アンバインド
        Container.BindInstances(1, 2, 3);
        Container.Unbind<int>();

        // Tを複数バインドしても一回のUnbind<T>()で全てアンバインドされる
        Assert.AreEqual(0, Container.ResolveAll<int>().Count);
        AssertThrows<ZenjectException>(() => Container.Resolve<int>());
        // -> Unable to resolve 'int'.
    }

Unbind()でバインド前の状態に戻すことができますが、ドキュメントによると

Note however that using methods are often a sign of bad practice.

らしいので、できれば使わないほうが良さそうです。

UnbindId()

    [Test]
    public void _07_UnbindId()
    {
        // idつきバインド
        var id = new object();
        Container.BindInstance("a").WithId(id);
        Assert.AreEqual("a", Container.ResolveId<string>(id));

        // UnbindId()でバインド前の状態に戻る(ResolveId()がエラーになる)
        Container.UnbindId<string>(id);
        AssertThrows<ZenjectException>(() => Container.ResolveId<string>(id));
        // -> Unable to resolve 'string'.

        // 複数バインド->アンバインド
        Container.BindInstance("b").WithId(id);
        Container.BindInstance("c").WithId(id);
        Container.UnbindId<string>(id);

        // Tをidつきで複数バインドしても一回のUnbindId<T>()で全てアンバインドされる
        Assert.AreEqual(0, Container.ResolveIdAll<string>(id).Count);
        AssertThrows<ZenjectException>(() => Container.ResolveId<string>(id));
        // -> Unable to resolve 'string'.
    }

UnbindId()はidを指定するだけでUnbind()とほぼ同じ。

UnbindAll()

    [Test]
    public void _08_UnbindAll()
    {
        Container.BindInstance("str");
        Container.BindInstance(1);
        Container.BindInstance(true);
        Container.BindInstance(100m).WithId("decimal");

        // UnbindAll()はバインド済みの全ての型をアンバインドする(idつき含む)
        Container.UnbindAll();
        AssertThrows<ZenjectException>(() => Container.Resolve<string>());
        AssertThrows<ZenjectException>(() => Container.Resolve<int>());
        AssertThrows<ZenjectException>(() => Container.Resolve<bool>());
        AssertThrows<ZenjectException>(() => Container.ResolveId<decimal>("decimal"));
        // -> Unable to resolve 'string'.
        // -> Unable to resolve 'int'.
        // -> Unable to resolve 'bool'.
        // -> Unable to resolve 'decimal (ID: decimal)'.
    }

未バインド時のアンバインド

    [Test]
    public void _09_UnbindWhenNoBindings()
    {
        // 何もバインドされていない状態での各アンバインドはエラーにならない
        Container.Unbind<string>();
        Container.Unbind<int>();
        Container.Unbind<bool>();
        Container.UnbindAll();
        Container.UnbindId<decimal>("");
    }

2. スコープ AsTransient(),AsCached(),AsSingle()

以下のクラスを使用します。

KlassWithId.cs
public class KlassWithId
{
    private static int Counter = 0;

    public static void ResetIdCounter()
    {
        Counter = 0;
    }

    public int id { get; private set; }

    public KlassWithId()
    {
        this.id = Counter++;
    }
}
_2_ScopeTest.cs
using NUnit.Framework;
using Zenject;

public class _2_ScopeTest : DiContainerTestBase
{
    public override void SetUp()
    {
        base.SetUp();
        KlassWithId.ResetIdCounter();
    }

    // ここにテストコードを書いていく
}

全文(GitHub)

AsTransient()

    [Test]
    public void _01_BindAsTransient()
    {
        // AsTransient()はResolve()ごとに新しくインスタンスが作られる
        Container.Bind<KlassWithId>().AsTransient();
        Assert.AreEqual(0, Container.Resolve<KlassWithId>().id);
        Assert.AreEqual(1, Container.Resolve<KlassWithId>().id);
        Assert.AreEqual(2, Container.Resolve<KlassWithId>().id);
    }

AsCached()

    [Test]
    public void _02_BindAsCached()
    {
        // AsCached()は最初のResolve()時にインスタンスを生成&キャッシュして以降のResolve()でも同じインスタンスを返す
        Container.Bind<KlassWithId>().AsCached();
        Assert.AreEqual(0, Container.Resolve<KlassWithId>().id);
        Assert.AreEqual(0, Container.Resolve<KlassWithId>().id);

        // バインドし直す(キャッシュクリアされる)
        Container.Unbind<KlassWithId>();
        Container.Bind<KlassWithId>().AsCached();

        // 新しいインスタンスがキャッシュされ、バインド前とは異なるインスタンスが得られる(id:1)
        Assert.AreEqual(1, Container.Resolve<KlassWithId>().id);
        Assert.AreEqual(1, Container.Resolve<KlassWithId>().id);

        // Rebind()はUnbind()->Bind()と同じ
        Container.Rebind<KlassWithId>().AsCached();

        // Rebind()前とは異なるインスタンスが得られる(id:2)
        Assert.AreEqual(2, Container.Resolve<KlassWithId>().id);
        Assert.AreEqual(2, Container.Resolve<KlassWithId>().id);
    }

複数回AsCached()でバインド

    [Test]
    public void _03_BindAsCachedMultiple()
    {
        // WARNING: AsCached()による複数バインドはエラーにならない(インスタンスがユニークであることは保証されない)
        Container.Bind<KlassWithId>().AsCached();
        Container.Bind<KlassWithId>().AsCached();
        Container.Bind<KlassWithId>().AsCached();

        // ResolveAll()はそれぞれ異なるインスタンスを返す(id:1,2,3)
        var list = Container.ResolveAll<KlassWithId>();
        Assert.AreEqual(list[0].id, 0);
        Assert.AreEqual(list[1].id, 1);
        Assert.AreEqual(list[2].id, 2);

        // 2回目以降のResolveAll()はそれぞれキャッシュされたインスタンスを返す(id:1,2,3)
        list = Container.ResolveAll<KlassWithId>();
        Assert.AreEqual(list[0].id, 0);
        Assert.AreEqual(list[1].id, 1);
        Assert.AreEqual(list[2].id, 2);

        // 複数バインドしているのでResolve()はエラー
        AssertThrows<ZenjectException>(() => Container.Resolve<KlassWithId>());
        // -> Found multiple matches when only one was expected for type 'KlassWithId'.
    }

AsCached()で複数バインドがエラーにならないのは意外。

AsSingle()

    [Test]
    public void _04_BindAsSingle()
    {
        // AsSingle()は何度Resolve()してもキャッシュされたインスタンスを返す
        Container.Bind<KlassWithId>().AsSingle();
        for (var i = 0; i < 5; i++)
        {
            Assert.AreEqual(0, Container.Resolve<KlassWithId>().id);
        }
    }

AsSingle()で3回バインド

    [Test]
    public void _05_BindAsSingle3Times()
    {
        // WARNING: 同じ型に対する2連続AsSingle()はエラーにならない
        Container.Bind<KlassWithId>().AsSingle();
        Container.Bind<KlassWithId>().AsSingle();

        // 次にFlushBindings()が呼ばれるタイミングでエラー
        AssertThrows<ZenjectException>(() => Container.Bind<KlassWithId>().AsSingle());
        // -> Assert hit! Attempted to use AsSingle multiple times for type 'KlassWithId'.
        //    As of Zenject 6+, AsSingle as can no longer be used for the same type across different bindings.
        //    See the upgrade guide for details.

        // 一度エラーになったらResolve()はエラーにならない
        Assert.AreEqual(0, Container.Resolve<KlassWithId>().id);
        Assert.AreEqual(0, Container.Resolve<KlassWithId>().id);
        Assert.AreEqual(0, Container.Resolve<KlassWithId>().id);
    }

AsSingle()で2回バインドしてResolve()

    [Test]
    public void _06_BindAsSingle2TimesAndResolve()
    {
        // WARNING: 同じ型に対する2連続AsSingle()はエラーにならない
        Container.Bind<KlassWithId>().AsSingle();
        Container.Bind<KlassWithId>().AsSingle();

        // 次にFlushBindings()が呼ばれるタイミングでエラー
        AssertThrows<ZenjectException>(() => Container.Resolve<KlassWithId>());
        // -> Assert hit! Attempted to use AsSingle multiple times for type 'KlassWithId'.
        //    As of Zenject 6+, AsSingle as can no longer be used for the same type across different bindings.
        //    See the upgrade guide for details.

        // 一度エラーになったら次からはエラーにならない
        Assert.AreEqual(0, Container.Resolve<KlassWithId>().id);
        Assert.AreEqual(0, Container.Resolve<KlassWithId>().id);
        Assert.AreEqual(0, Container.Resolve<KlassWithId>().id);
    }

AsSingle()で2回バインドしてResolveAll()

    [Test]
    public void _07_BindAsSingle2TimesAndResolveAll()
    {
        // WARNING: 同じ型に対する2連続AsSingle()はエラーにならない
        Container.Bind<KlassWithId>().AsSingle();
        Container.Bind<KlassWithId>().AsSingle();

        // 次にFlushBindings()が呼ばれるタイミングでエラー
        AssertThrows<ZenjectException>(() => Container.ResolveAll<KlassWithId>());
        // -> Assert hit! Attempted to use AsSingle multiple times for type 'KlassWithId'.
        //    As of Zenject 6+, AsSingle as can no longer be used for the same type across different bindings.
        //    See the upgrade guide for details.

        // 一度エラーになったら次からはエラーにならない
        Assert.AreEqual(1, Container.ResolveAll<KlassWithId>().Count);
        Assert.AreEqual(1, Container.ResolveAll<KlassWithId>().Count);
        Assert.AreEqual(1, Container.ResolveAll<KlassWithId>().Count);
    }

AsSingle()でバインド・アンバインド

    [Test]
    public void _08_BindAsSingleMultipleWithUnbind()
    {
        Container.Bind<KlassWithId>().AsSingle();
        Container.Unbind<KlassWithId>();

        // Unbind()したのでResolve()でエラー
        AssertThrows<ZenjectException>(() => Container.Resolve<KlassWithId>());
        // -> Unable to resolve 'KlassWithId'.

        // バインドし直してからのResolve()もエラー
        Container.Bind<KlassWithId>().AsSingle();
        AssertThrows<ZenjectException>(() => Container.Resolve<KlassWithId>());
        // -> Assert hit! Attempted to use AsSingle multiple times for type 'KlassWithId'.
        //    As of Zenject 6+, AsSingle as can no longer be used for the same type across different bindings.
        //    See the upgrade guide for details.
    }

AsSingle()はバインド2回以上したりアンバインドしちゃいけないみたいです。


3.ペアレンティング

次のテストクラスを使用します。

_3_ParentingTest.cs
using NUnit.Framework;
using Zenject;

public class _3_ParentingTest : DiContainerTestBase
{
    private DiContainer AncestorContainer;
    private DiContainer ParentContainer;
    private DiContainer SelfContainer;
    private DiContainer ChildContainer;

    public override void SetUp()
    {
        // ペアレンティング
        // Ancestor
        //  ↑
        // Parent
        //  ↑
        // Self
        //  ↑
        // Child
        AncestorContainer = new DiContainer();
        ParentContainer = new DiContainer(AncestorContainer);
        SelfContainer = new DiContainer(ParentContainer);
        ChildContainer = new DiContainer(SelfContainer);
    }

    // ここにテストコードを書いていく
}

全文(GitHub)

ParentContainers

    [Test]
    public void _01_ParentContainers()
    {
        // ParentContainersには直接の親コンテナが入っている
        Assert.AreEqual(1, SelfContainer.ParentContainers.Length);
        Assert.AreEqual(ParentContainer, SelfContainer.ParentContainers[0]);
    }

AncestorContainers

    [Test]
    public void _02_AncestorContainers()
    {
        // AncestorContainersには親を辿って得られる全てのコンテナが入っている
        Assert.AreEqual(2, SelfContainer.AncestorContainers.Length);
        Assert.AreEqual(ParentContainer, SelfContainer.AncestorContainers[0]);
        Assert.AreEqual(AncestorContainer, SelfContainer.AncestorContainers[1]);
    }

複数の親/祖先コンテナ

    [Test]
    public void _03_MultipleParentsAndAncestors()
    {
        // ↓こんな感じのペアレンティングツリー
        // [p0a0,p0a1] [p1a0] [p2a0,p2a1]
        //         \     |     /
        //          [p0, p1, p2]
        //            \  |  /
        //              self

        var p0a0 = new DiContainer();
        var p0a1 = new DiContainer();
        // 親コンテナは複数持てる
        var p0 = new DiContainer(new[] {p0a0, p0a1});

        var p1a0 = new DiContainer();
        var p1 = new DiContainer(new[] {p1a0});

        var p2a0 = new DiContainer();
        var p2a1 = new DiContainer();
        var p2 = new DiContainer(new[] {p2a0, p2a1});

        var self = new DiContainer(new[] {p0, p1, p2});

        // ParentContainersには直接の親コンテナが全て入っている
        Assert.AreEqual(3, self.ParentContainers.Length);
        Assert.AreEqual(p0, self.ParentContainers[0]);
        Assert.AreEqual(p1, self.ParentContainers[1]);
        Assert.AreEqual(p2, self.ParentContainers[2]);

        // AncestorContainersには親を辿って得られる全てのコンテナが入っている(順番は幅優先探索)
        Assert.AreEqual(8, self.AncestorContainers.Length);
        Assert.AreEqual(p0, self.AncestorContainers[0]);
        Assert.AreEqual(p1, self.AncestorContainers[1]);
        Assert.AreEqual(p2, self.AncestorContainers[2]);
        Assert.AreEqual(p0a0, self.AncestorContainers[3]);
        Assert.AreEqual(p0a1, self.AncestorContainers[4]);
        Assert.AreEqual(p1a0, self.AncestorContainers[5]);
        Assert.AreEqual(p2a0, self.AncestorContainers[6]);
        Assert.AreEqual(p2a1, self.AncestorContainers[7]);
    }

親コンテナへのバインド

    [Test]
    public void _04_BindParentAndSelfResolve()
    {
        // バインドしていないのでエラー
        AssertThrows<ZenjectException>(() => SelfContainer.Resolve<string>());
        // -> Unable to resolve 'string'.

        // 親コンテナにバインドされていれば自コンテナのResolve()は親を辿って解決される
        ParentContainer.BindInstance("parent");
        Assert.AreEqual("parent", SelfContainer.Resolve<string>());

        // 自コンテナにバインドされていればそのまま解決される
        SelfContainer.BindInstance("self");
        Assert.AreEqual("self", SelfContainer.Resolve<string>());

        // ResolveAll()による解決の探索には親コンテナツリーも含まれる
        var list = SelfContainer.ResolveAll<string>();
        Assert.AreEqual(2, list.Count);
        Assert.AreEqual("self", list[0]);
        Assert.AreEqual("parent", list[1]);
    }

祖先コンテナへのバインド

    [Test]
    public void _05_BindAncestorAndSelfResolve()
    {
        // バインドしていないのでエラー
        AssertThrows<ZenjectException>(() => SelfContainer.Resolve<string>());
        // -> Unable to resolve 'string'.

        // 親コンテナツリーのどこかにバインドされていれば自コンテナのResolve()は親を辿って解決される
        AncestorContainer.BindInstance("ancestor");
        Assert.AreEqual("ancestor", SelfContainer.Resolve<string>());

        // より親コンテナツリーの中で近いコンテナ優先で解決される
        ParentContainer.BindInstance("parent");
        Assert.AreEqual("parent", SelfContainer.Resolve<string>());
    }

ペアレンティングによるバインドの解決は、どうやらJavaScriptのプロトタイプチェーンとかRubyのメソッド探索みたいな仕組みらしいです。

複数の親コンテナへのバインド

    [Test]
    public void _06_MultipleParentsAndSelfResolve()
    {
        var parents = new[]
        {
            new DiContainer(),
            new DiContainer(),
            new DiContainer(),
        };

        var self = new DiContainer(parents);

        // 親コンテナ群にそれぞれバインド
        parents[0].BindInstance("p0");
        parents[1].BindInstance("p1");
        parents[2].BindInstance("p2");

        // ResolveAll<T>()は親コンテナ群にバインドされたものを集めてList<T>で返す
        var list = self.ResolveAll<string>();
        Assert.AreEqual(3, list.Count);
        Assert.AreEqual("p0", list[0]);
        Assert.AreEqual("p1", list[1]);
        Assert.AreEqual("p2", list[2]);

        // Resolve()はエラー(複数の親コンテナは一つの親コンテナとして振る舞う)
        AssertThrows<ZenjectException>(() => self.Resolve<string>());
        // -> Found multiple matches when only one was expected for type 'string'.

        // 自コンテナにバインドされていればエラーにならずに解決される
        self.BindInstance("self");
        Assert.AreEqual("self", self.Resolve<string>());

        // 自コンテナにバインドしたものはResolveAll()のListの先頭にくる
        list = self.ResolveAll<string>();
        Assert.AreEqual(4, list.Count);
        Assert.AreEqual("self", list[0]);
        Assert.AreEqual("p0", list[1]);
        Assert.AreEqual("p1", list[2]);
        Assert.AreEqual("p2", list[3]);
    }

複数の親コンテナは一つの親コンテナとして振る舞う(←重要)

複数の祖先コンテナへのバインド

    [Test]
    public void _07_MultipleAncestorsAndSelfResolve()
    {
        // ↓こんな感じのペアレンティングツリー
        // [p0a0,p0a1] [p1a0] [p2a0,p2a1]
        //         \     |     /
        //          [p0, p1, p2]
        //            \  |  /
        //              self
        // (省略)

        // 祖先コンテナにバインド
        p1a0.BindInstance("p0a0");
        Assert.AreEqual("p0a0", self.Resolve<string>());

        // 別の親を子として持つ祖先コンテナにバインド
        p1a0.BindInstance("p1a0");

        // Resolve()は"Found multiple matches"のエラー(深さが同じ複数の祖先コンテナは一つの祖先コンテナとして振る舞う)
        AssertThrows<ZenjectException>(() => self.Resolve<string>());
        // -> Found multiple matches when only one was expected for type 'string'.

        // ResolveAll<T>()はペアレンティングツリーにバインドされたもの全てを集めてList<T>で返す(幅優先探索で解決)
        p2.BindInstance("p2");
        var list = self.ResolveAll<string>();
        Assert.AreEqual(3, list.Count);
        Assert.AreEqual("p2", list[0]);
        Assert.AreEqual("p0a0", list[1]);
        Assert.AreEqual("p1a0", list[2]);
    }

複数の同じ深さの祖先コンテナは一つのコンテナとして振る舞う(←重要)

複数の親/祖先コンテナへのバインド

    [Test]
    public void _08_MultipleParentsAndAncestorsAndSelfResolve()
    {
        // ↓こんな感じのペアレンティングツリー
        // [p0a0,p0a1] [p1a0] [p2a0,p2a1]
        //         \     |     /
        //          [p0, p1, p2]
        //            \  |  /
        //              self
        // (省略)

        // 祖先コンテナにバインド
        p1a0.BindInstance("p1a0");
        Assert.AreEqual("p1a0", self.Resolve<string>());

        // 親コンテナにバインド
        // WARNING: 深さが同じコンテナ群にバインドした場合と違ってエラーにならない
        p0.BindInstance("p0");
        Assert.AreEqual("p0", self.Resolve<string>());

        // ResolveAll<T>()はペアレンティングツリーにバインドされたもの全てを集めてList<T>で返す(幅優先探索で解決)
        var list = self.ResolveAll<string>();
        Assert.AreEqual(2, list.Count);
        Assert.AreEqual("p0", list[0]);
        Assert.AreEqual("p1a0", list[1]);
    }
  • 深さが異なる親/祖先コンテナは別のコンテナとして振る舞う(←重要)
  • インスタンスの解決には浅い親コンテナが優先される

Unbind(),UnbindAll()が親コンテナツリーの探索に与える影響

    [Test]
    public void _09_UnbindAndUnbindAll()
    {
        // Unbind(),UnbindAll()はResolve()時の親コンテナツリーの探索に影響を与えない
        ParentContainer.BindInstance("parent");
        SelfContainer.Unbind<string>();
        Assert.AreEqual("parent", SelfContainer.Resolve<string>());
        SelfContainer.UnbindAll();
        Assert.AreEqual("parent", SelfContainer.Resolve<string>());

        // ペアレンティングが解除されたりもしない
        Assert.AreEqual(1, SelfContainer.ParentContainers.Length);
        Assert.AreEqual(ParentContainer, SelfContainer.ParentContainers[0]);

        Assert.AreEqual(2, SelfContainer.AncestorContainers.Length);
        Assert.AreEqual(ParentContainer, SelfContainer.AncestorContainers[0]);
        Assert.AreEqual(AncestorContainer, SelfContainer.AncestorContainers[1]);
    }

子コンテナが自コンテナに与える影響

    [Test]
    public void _10_BindChildAndSelfResolve()
    {
        // 子コンテナのバインドは親コンテナへ影響を与えない
        ChildContainer.BindInstance("child");
        AssertThrows<ZenjectException>(() => SelfContainer.Resolve<string>());
        // -> Unable to resolve 'string'.
        Assert.AreEqual(0, SelfContainer.ResolveAll<string>().Count);
    }

4.プリミティブでないクラス/インターフェース

次のテストクラスを使用します。

_4_BindClassAndInterfaceTest.cs
using NUnit.Framework;
using Zenject;

public class _4_BindClassAndInterfaceTest : DiContainerTestBase
{
    public override void SetUp()
    {
        base.SetUp();
        KlassWithId.ResetIdCounter();
    }

    public interface IFoo
    {
        int id { get; }
        string name { get; }
    }

    public interface IBar
    {
    }

    public class FooBase : KlassWithId, IFoo, IBar
    {
        public virtual string name
        {
            get { return "FooBase"; }
        }
    }

    public class Foo : FooBase
    {
        public override string name
        {
            get { return "Foo"; }
        }
    }
    // ここにテストコードを書いていく
}

全文(GitHub)

基底クラスを派生クラスにバインド

    [Test]
    public void _01_BindBaseClass()
    {
        // 基底クラスを派生クラスにバインド
        Container.Bind<FooBase>().To<Foo>().AsTransient();

        // バインドしたクラスは解決できる(中身はバインド先クラスのインスタンス)
        FooBase fooBase = Container.Resolve<FooBase>();
        Assert.AreEqual("Foo", fooBase.name);

        // バインド先クラスは直接解決できない
        AssertThrows<ZenjectException>(() => Container.Resolve<Foo>());
        // -> Unable to resolve '_4_BindClassAndInterfaceTest.Foo'.
    }

インターフェースをその実装クラスにバインド

    [Test]
    public void _02_BindInterface()
    {
        // インターフェースをその実装クラスにバインド
        Container.Bind<IFoo>().To<Foo>().AsTransient();

        // インターフェースは解決できる(中身はバインド先クラスのインスタンス)
        IFoo ifoo = Container.Resolve<IFoo>();
        Assert.AreEqual("Foo", ifoo.name);

        // バインド先クラスは直接解決できない
        AssertThrows<ZenjectException>(() => Container.Resolve<Foo>());
        // -> Unable to resolve '_4_BindClassAndInterfaceTest.Foo'.
    }

バインドしたクラスの基底クラス/実装インターフェースを解決

    [Test]
    public void _03_ResolveBaseClassAndInterface()
    {
        // バインドしたクラスの基底クラスおよびインターフェースは直接解決できない
        Container.Bind<Foo>().AsTransient();
        AssertThrows<ZenjectException>(() => Container.Resolve<FooBase>());
        AssertThrows<ZenjectException>(() => Container.Resolve<IFoo>());
        // -> Unable to resolve '_4_BindClassAndInterfaceTest.FooBase'.
        // -> Unable to resolve '_4_BindClassAndInterfaceTest.IFoo'.
    }

AsTransient()によるバインド

    [Test]
    public void _04_AsTransientAndResolve()
    {
        // AsTransient()でそれぞれバインド
        Container.Bind<FooBase>().To<Foo>().AsTransient();
        Container.Bind<IFoo>().To<Foo>().AsTransient();
        FooBase fooBase = Container.Resolve<FooBase>();
        IFoo ifoo = Container.Resolve<IFoo>();

        // どちらもFooのインスタンス
        Assert.AreEqual("Foo", fooBase.name);
        Assert.AreEqual("Foo", ifoo.name);

        // インスタンスは異なる
        Assert.AreEqual(0, fooBase.id);
        Assert.AreEqual(1, ifoo.id);
    }

AsCached()+FromResolve()

    [Test]
    public void _05_AsCachedAndFromResolveAndResolve()
    {
        // AsCached()でバインド
        Container.Bind<Foo>().AsCached();

        // FromResolve()によってFooへのバインドは既存のバインドから解決
        Container.Bind<FooBase>().To<Foo>().FromResolve();
        Container.Bind<IFoo>().To<Foo>().FromResolve();

        Foo foo = Container.Resolve<Foo>();
        FooBase fooBase = Container.Resolve<FooBase>();
        IFoo ifoo = Container.Resolve<IFoo>();

        // 全てFooのインスタンス
        Assert.AreEqual("Foo", foo.name);
        Assert.AreEqual("Foo", fooBase.name);
        Assert.AreEqual("Foo", ifoo.name);

        // AsCached() + FromResolve() によって全て同じインスタンス
        Assert.AreEqual(0, foo.id);
        Assert.AreEqual(0, fooBase.id);
        Assert.AreEqual(0, ifoo.id);
    }

BindInterfacesTo()

    [Test]
    public void _06_BindInterfaces()
    {
        // バインドするクラスが実装しているインターフェースを全てバインド
        Container.BindInterfacesTo<Foo>().AsCached();

        // バインド先クラスの全てのインターフェースを解決できる(AsCached()なので同じインスタンス)
        Assert.AreSame(Container.Resolve<IFoo>(), Container.Resolve<IBar>());

        // バインド先クラスは直接解決できない
        AssertThrows<ZenjectException>(() => Container.Resolve<Foo>());
        // -> Unable to resolve '_4_BindClassAndInterfaceTest.Foo'.

        // 基底クラスは直接解決できない
        AssertThrows<ZenjectException>(() => Container.Resolve<FooBase>());
        // -> Unable to resolve '_4_BindClassAndInterfaceTest.FooBase'.
    }

BindInterfacesAndSelfTo()

    [Test]
    public void _07_BindInterfacesAndSelfTo()
    {
        // クラスとそのクラスが実装しているインターフェース全てをバインド
        Container.BindInterfacesAndSelfTo<Foo>().AsCached();

        // バインドしたクラスもインターフェースも解決できる(AsCached()なので同じインスタンス)
        Assert.AreSame(Container.Resolve<Foo>(), Container.Resolve<IFoo>());
        Assert.AreSame(Container.Resolve<Foo>(), Container.Resolve<IBar>());

        // 基底クラスは直接解決できない
        AssertThrows<ZenjectException>(() => Container.Resolve<FooBase>());
        // -> Unable to resolve '_4_BindClassAndInterfaceTest.FooBase'.
    }

5.コンストラクタ引数を持つクラスの解決

次のテストクラスを使用します。

_5_ArgumentsTest.cs
using System.Collections.Generic;
using NUnit.Framework;
using Zenject;

public class _5_ArgumentsTest : DiContainerTestBase
{
    public class Fizz
    {
        public readonly int id;
        public readonly string name;

        public Fizz(int id, string name)
        {
            this.id = id;
            this.name = name;
        }
    }

    // ここにテストコードを書いていく
}

全文(GitHub)

WithArguments()

    [Test]
    public void _01_WithArguments()
    {
        // WithArgumentsでコンストラクタ引数も一緒にバインド
        const int id = 100;
        const string name = "fizz";
        Container.Bind<Fizz>().AsTransient().WithArguments(id, name);

        // インスタンスの生成でバインド時に指定したコンストラクタ引数が使われる
        var bar = Container.Resolve<Fizz>();
        Assert.AreEqual(id, bar.id);
        Assert.AreEqual(name, bar.name);

        // 実際のコンストラクタ引数の順番と違う順番でWithArguments()に渡しても正しく動く
        Container.UnbindAll();
        Container.Bind<Fizz>().AsTransient().WithArguments(name, id);
        bar = Container.Resolve<Fizz>();
        Assert.AreEqual(id, bar.id);
        Assert.AreEqual(name, bar.name);
    }

コンストラクタ引数なし

    [Test]
    public void _02_NoArguments()
    {
        Container.Bind<Fizz>().AsTransient();

        // インスタンス生成時にコンストラクタ引数が解決できないのでエラー
        AssertThrows<ZenjectException>(() => Container.Resolve<Fizz>());
        // -> Unable to resolve 'int' while building object with type '_5_ArgumentsTest.Fizz'.
    }

WithArguments()を使わないコンストラクタ引数の解決

    [Test]
    public void _03_NoArgumentsButBind()
    {
        Container.Bind<Fizz>().AsTransient();

        // コンストラクタ引数と同じ型のインスタンスをそれぞれバインド
        const int id = 100;
        const string name = "fizz";
        Container.BindInstance(id);
        Container.BindInstance(name);

        // WARNING: コンストラクタ引数がバインドしたコンテキストから解決される!
        var bar = Container.Resolve<Fizz>();
        Assert.AreEqual(id, bar.id);
        Assert.AreEqual(name, bar.name);

        // コンストラクタ引数と型が合致するインスタンスが複数バインドされているとエラー
        Container.BindInstance("str");
        AssertThrows<ZenjectException>(() => Container.Resolve<Fizz>());
        // -> Found multiple matches when only one was expected for type 'string' while building object with type '_5_ArgumentsTest.Fizz'.
    }

Listをコンストラクタ引数として持つクラスの解決

    public class Buzz
    {
        public readonly List<int> numbers;

        public Buzz(List<int> numbers)
        {
            this.numbers = numbers;
        }
    }

    [Test]
    public void _04_ListArguments()
    {
        // コンストラクタ引数として使えるバインドがないのでエラー
        AssertThrows<ZenjectException>(() => Container.Resolve<Buzz>());

        Container.Bind<Buzz>().AsTransient();
        Container.BindInstance(new List<int> {10, 20, 30});
        var buzz = Container.Resolve<Buzz>();
        Assert.AreEqual(3, buzz.numbers.Count);
        Assert.AreEqual(10, buzz.numbers[0]);
        Assert.AreEqual(20, buzz.numbers[1]);
        Assert.AreEqual(30, buzz.numbers[2]);

        // コンストラクタ引数になるインスタンスをアンバインド
        Assert.AreEqual(3, Container.Resolve<List<int>>().Count);
        Container.Unbind<List<int>>();
        Assert.AreEqual(0, Container.Resolve<List<int>>().Count);

        // WARNING: クラスの解決はエラーにならない(空のListとして解決される)
        Assert.AreEqual(0, Container.Resolve<Buzz>().numbers.Count);

        // コンストラクタ引数List<T>のTをListを使わず複数バインド
        Container.BindInstance(100);
        Container.BindInstances(200, 300);
        buzz = Container.Resolve<Buzz>();

        // 複数バインドしたインスタンスがList<T>として解決される
        Assert.AreEqual(3, buzz.numbers.Count);
        Assert.AreEqual(100, buzz.numbers[0]);
        Assert.AreEqual(200, buzz.numbers[1]);
        Assert.AreEqual(300, buzz.numbers[2]);
    }

最後に

プレハブのバインドなどUnityに関係したメソッドについては確認できていないのですが、長くなってしまったのでこの辺で。

DiContainerの挙動がだいたいわかったので、次はペアレンティングやidを使った適切なスコープの切り方について検討/解説していきたいです。

29
22
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
29
22