VContainer の Singleton/Scoped の挙動をみる
TL;DR
- 私の理解が合っているかを確かめるために VContainer の挙動をみた
- Singleton:
-
Register()したスコープとその子孫のスコープで 1 つのインスタンスを使う - 子と親で同じ型を
Register()できて、その時は自身か最も近い親のインスタンスを使う
-
- Scoped:
- 各スコープで1つのインスタンスを使う
- つまり、親と子では別々のインスタンスを使う
- Singleton 同様に自身か最も近い親の
Register()を見てインスタンス化する
- 各スコープで1つのインスタンスを使う
準備
環境:
- 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 で、 TestScript1 を Singleton として 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() はそのままにして、 Child1LifetimeScope と Child3LifetimeScope でそれぞれ 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 を消して、 TestScript1 を Scoped として 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() を探し」の挙動を確認するために、 TestScript1 に IStartable を実装させて、 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度だけ呼ばれ instanceID も Child3LifetimeScope と一致しているので、 Child3LifetimeScope が自身の Register() に従って TestScript1 をインスタンス化したことが分かる。

まとめ
VContainer のドキュメントの内容を VContainer を動かして確認した。
- Singleton:
-
Register()したスコープとその子孫のスコープで 1 つのインスタンスを使う - 子と親で同じ型を
Register()できて、その時は自身か最も近い親のインスタンスを使う
-
- Scoped:
- 各スコープで1つのインスタンスを使う
- つまり、親と子では別々のインスタンスを使う
-
Singleton同様に自身か最も近い親のRegister()を見てインスタンス化する
- 各スコープで1つのインスタンスを使う
Discussion