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