🎉

VContainer の Singleton/Scoped の挙動をみる

に公開

TL;DR

  • 私の理解が合っているかを確かめるために VContainer の挙動をみた
  • Singleton:
    • Register() したスコープとその子孫のスコープで 1 つのインスタンスを使う
    • 子と親で同じ型を Register() できて、その時は自身か最も近い親のインスタンスを使う
  • Scoped:
    • 各スコープで1つのインスタンスを使う
      • つまり、親と子では別々のインスタンスを使う
    • Singleton 同様に自身か最も近い親の Register() を見てインスタンス化する

準備

環境:

  • Unity: 6000.0.25f1
  • VContainer: v1.17.0
コード全文
public class TestScript1
{
    readonly Guid instanceID;
    public string InstanceID => instanceID.ToString()[..7];

    public TestScript1()
    {
        instanceID = Guid.NewGuid();
    }
}

public class TestScript2 : IStartable
{
    readonly TestScript1 testScript1;
    readonly string lifetimeScopeName;

    public TestScript2(
        TestScript1 testScript1,
        string lifetimeScopeName)
    {
        this.testScript1 = testScript1;
        this.lifetimeScopeName = lifetimeScopeName;
    }

    void IStartable.Start()
    {
        Debug.Log($"{lifetimeScopeName} {testScript1.InstanceID}");
    }
}

public class ParentLifetimeScope : LifetimeScope
{
    protected override void Configure(IContainerBuilder builder)
    {
        builder.Register<TestScript1>(Lifetime.Singleton);
        //builder.Register<TestScript1>(Lifetime.Scoped);
        //builder.Register<TestScript1>(Lifetime.Transient);

        builder.Register<TestScript2>(Lifetime.Singleton)
            .AsImplementedInterfaces()
            .WithParameter("lifetimeScopeName", gameObject.name);
    }
}

public class Child1LifetimeScope : LifetimeScope
{
    protected override void Configure(IContainerBuilder builder)
    {
        builder.Register<TestScript2>(Lifetime.Singleton)
            .AsImplementedInterfaces()
            .WithParameter("lifetimeScopeName", gameObject.name);
    }
}

public class Child2LifetimeScope : LifetimeScope
{
    protected override void Configure(IContainerBuilder builder)
    {
        builder.Register<TestScript2>(Lifetime.Singleton)
            .AsImplementedInterfaces()
            .WithParameter("lifetimeScopeName", gameObject.name);
    }
}

public class Child3LifetimeScope : LifetimeScope
{
    protected override void Configure(IContainerBuilder builder)
    {
        builder.Register<TestScript2>(Lifetime.Singleton)
            .AsImplementedInterfaces()
            .WithParameter("lifetimeScopeName", gameObject.name);
    }
}

TestScript1 がどのようにインスタンス化されるかをみていく。 LifetimeScope の親子関係は上の画像の GameObject の親子関係と同じにしている。

Lifetime.Singleton の挙動

まずは ParentLifetimeScope で、 TestScript1Singleton として Register() する。

public class ParentLifetimeScope : LifetimeScope
{
    protected override void Configure(IContainerBuilder builder)
    {
        builder.Register<TestScript1>(Lifetime.Singleton);

        builder.Register<TestScript2>(Lifetime.Singleton)
            .AsImplementedInterfaces()
            .WithParameter("lifetimeScopeName", gameObject.name);
    }
}

この場合は単純で、 VContainer のドキュメントにある通り ParentLifetimeScope 以下で同じインスタンスを使う。

次は ParentLifetimeScope での Register() はそのままにして、 Child1LifetimeScopeChild3LifetimeScope でそれぞれ Singleton として Register() する。

public class Child1LifetimeScope : LifetimeScope
{
    protected override void Configure(IContainerBuilder builder)
    {
        builder.Register<TestScript1>(Lifetime.Singleton);

        builder.Register<TestScript2>(Lifetime.Singleton)
            .AsImplementedInterfaces()
            .WithParameter("lifetimeScopeName", gameObject.name);
    }
}

public class Child3LifetimeScope : LifetimeScope
{
    protected override void Configure(IContainerBuilder builder)
    {
        builder.Register<TestScript1>(Lifetime.Singleton);

        builder.Register<TestScript2>(Lifetime.Singleton)
            .AsImplementedInterfaces()
            .WithParameter("lifetimeScopeName", gameObject.name);
    }
}

これもVContainer のドキュメントにある通り、子で Register() していれば親のインスタンスではなく子の Register() をみてインスタンスを生成して使う。

  • ParentLifetimeScope
  • Child1LifetimeScope 以下のすべての Scope
  • Child3LifetimeScope

でそれぞれ 1 つずつ TestScript1 のインスタンスが生成されているのが分かる。

最後に、 ParentLifetimeScope を2つ複製して挙動を見てみる。

最初の 3 行に複製した ParentLifetimeScope のログが並んでいて instanceID が異なるので、同じ型の LifetimeScope があるときは別々のインスタンスを使うことが分かる。

Lifetime.Scoped の挙動

複製した ParentLifetimeScope を消して、 TestScript1Scoped として Register() して挙動をみる。

public class ParentLifetimeScope : LifetimeScope
{
    protected override void Configure(IContainerBuilder builder)
    {
        builder.Register<TestScript1>(Lifetime.Scoped);

        builder.Register<TestScript2>(Lifetime.Singleton)
            .AsImplementedInterfaces()
            .WithParameter("lifetimeScopeName", gameObject.name);
    }
}

public class Child1LifetimeScope : LifetimeScope
{
    protected override void Configure(IContainerBuilder builder)
    {
        builder.Register<TestScript2>(Lifetime.Singleton)
            .AsImplementedInterfaces()
            .WithParameter("lifetimeScopeName", gameObject.name);
    }
}

public class Child3LifetimeScope : LifetimeScope
{
    protected override void Configure(IContainerBuilder builder)
    {
        builder.Register<TestScript2>(Lifetime.Singleton)
            .AsImplementedInterfaces()
            .WithParameter("lifetimeScopeName", gameObject.name);
    }
}

Lifetime.Scoped は「自身かもっとも近い祖先の Register() を探し、コンテナはそれぞれがオブジェクトを生成して保持する」という挙動をする。ここでは ParentLifetimeScope のみで Register() しているので、 LifetimeScope ごとに別々のインスタンスを作っているのが分かる。

ここで「自身かもっとも近い祖先の Register() を探し」の挙動を確認するために、 TestScript1IStartable を実装させて、 Child3LifetimeScope でのみ実行されることをみる。

public class TestScript1 : IStartable
{
    readonly Guid instanceID;
    public string InstanceID => instanceID.ToString()[..7];

    public TestScript1()
    {
        instanceID = Guid.NewGuid();
    }

    void IStartable.Start()
    {
        Debug.Log($"{instanceID} IStartable.Start()");
    }
}

public class Child3LifetimeScope : LifetimeScope
{
    protected override void Configure(IContainerBuilder builder)
    {
        builder.Register<TestScript1>(Lifetime.Scoped)
            .AsImplementedInterfaces()
            .AsSelf();

        builder.Register<TestScript2>(Lifetime.Singleton)
            .AsImplementedInterfaces()
            .WithParameter("lifetimeScopeName", gameObject.name);
    }
}

IStartable.Start() が1度だけ呼ばれ instanceIDChild3LifetimeScope と一致しているので、 Child3LifetimeScope が自身の Register() に従って TestScript1 をインスタンス化したことが分かる。

まとめ

VContainer のドキュメントの内容を VContainer を動かして確認した。

  • Singleton:
    • Register() したスコープとその子孫のスコープで 1 つのインスタンスを使う
    • 子と親で同じ型を Register() できて、その時は自身か最も近い親のインスタンスを使う
  • Scoped:
    • 各スコープで1つのインスタンスを使う
      • つまり、親と子では別々のインスタンスを使う
    • Singleton 同様に自身か最も近い親の Register() を見てインスタンス化する
GitHubで編集を提案

Discussion