VContainer is 何?
タイトルの通りです。現在Unityで使えるDIライブラリにはZenject/Extenjectがありますが、それとは別の選択肢としてVContainerが作られています。なんでもZenject/Extenjectは多機能で便利なのですが、もっとシンプルな薄いDIライブラリを目指されているのだとか。作者はハダシA氏です。
https://github.com/hadashiA/VContainer
現在、開発中なのですが、0.0.1がリリースされたので早速触って見ました。
0.0.2が出ました(7/4)。InjectとIInitialize関係が更新されました。
まずはZenject/Extenjectで作る
namespace ZenjectSayHello
{
public interface IPerson
{
string Name { get; }
}
}
namespace ZenjectSayHello
{
public class Parent : IPerson
{
public string Name => "親御";
}
}
using UnityEngine;
namespace ZenjectSayHello
{
public class SayHello
{
public SayHello(IPerson person)
{
Debug.Log($"{person.Name}さんによろしく!");
}
}
}
using Zenject;
namespace ZenjectSayHello
{
public class SayHelloInstaller : MonoInstaller
{
public override void InstallBindings()
{
Container.Bind<IPerson>().To<Parent>().AsSingle();
Container.Bind<SayHello>().AsSingle();
}
}
}
using UnityEngine;
using Zenject;
namespace ZenjectSayHello
{
public class SayHelloMonoBehaviour : MonoBehaviour
{
[Inject] private SayHello _sayHello = default;
}
}
SayHelloMonoBehaviourは適当なGameObjectにアタッチします。SayHelloがResolveされるときに、コンストラクタのIPersonにParentが放り込まれ、
と出ます。こいつをVContainerで書いてみます。
VContainerで書く
インストール
まずインストール方法ですが、上記Githubリンクのリリースページにunitypackageがありますので、それをダウンロード、インストールするだけでOKです。
インストーラーを作る
最初から手で書いてもいいのですが、テンプレートを自動生成してくれます。Projectの右クリックからCreate > C# Script。で、ファイル名をXXXInstallerとすれば、Installerのテンプレートを作ってくれます。
LifetimeScopeを作る
Zenject/ExtenjectのSceneContext等にあたるものがLifetimeScopeになります。作り方はHierarchyの右クリックでVContainer > LifetimeScope。で、InspectorのMonoInstallerの+ボタンをクリックすると、さっき作ったSayHellowInstallerが出てきます。指定したInstallerはLifetimeScopeGameObjectにアタッチされます。
コード書く
コードを書いていきます。
namespace VContainerSayHello
{
public interface IPerson
{
string Name { get; }
}
}
namespace VContainerSayHello
{
public class Parent : IPerson
{
public string Name => "親御";
}
}
using UnityEngine;
namespace VContainerSayHello
{
public class SayHello
{
public SayHello(IPerson person)
{
Debug.Log($"{person.Name}さんによろしく!");
}
}
}
ここまではさっきと同じです。
using UnityEngine;
using VContainer;
namespace VContainerSayHello
{
public class SayHelloMonoBehaviour : MonoBehaviour
{
[Inject]
public void Construct(SayHello sayHello)
{
}
}
}
MonoBehaviourです。エディター上で適当なGameObjectを作って、それにアタッチします。サンプルではメソッドインジェクションがありましたので、そちらにしました。
using UnityEngine;
using VContainer;
using VContainer.Unity;
namespace VContainerSayHello
{
public sealed class SayHelloInstaller : MonoInstaller
{
[SerializeField] private SayHelloMonoBehaviour sayHello = default;
public override void Install(IContainerBuilder builder)
{
builder.Register<IPerson, Parent>(Lifetime.Singleton);
builder.Register<SayHello>(Lifetime.Singleton);
builder.RegisterComponent(sayHello);
}
}
}
キモとなる部分です。書き加えたのはInstallメソッド内の3行とsayHelloフィールド。
builder.Register<IPerson, Parent>(Lifetime.Singleton);
builder.Register<SayHello>(Lifetime.Singleton);
この2行は、ParentとSayHelloをZenject/ExtenjectでいうところのBindします。
[SerializeField] private SayHelloMonoBehaviour sayHello = default;
builder.RegisterComponent(sayHello);
その次の行とsayHelloのインスタンスです。この解説の前に、まずInjectの挙動について解説します。
[Inject]の挙動
Zenject/ExtenjectだとMonoBehaviourにInjectを書いておけば、どこでもInjectしてくれました。VContainerはエディター上でGameObjectを作ってHierarchyに存在してるだけではInjectを発動してくれません。
VContainerでは、Injectは原則的にResolveするときに発火します。例えばなんらかのクラスのコンストラクタ引数になっててそこに放り込まれる時とか、IInitializableが実装されててそれが走る時とか。
DIはコンストラクタでのインジェクションが原則だと言われます。コンストラクタインジェクションはResolveされる時以外はなんらInjectされないので、それとタイミングを合わせていると考えれば納得です。
[Inject]を発火させる方法
ですんで、IInitializableをSayHelloMonoBehaviourに実装してAsを使って発火するか、適当なコンストラクタにぶち込むか、なんですが(7/4追記に転記)、もっと簡単なやり方があります。それが上述の
builder.RegisterComponent(sayHello);
です。これでsayHelloインスタンスをInjectしてくれます。
※0.0.2で実装されました。0.0.1でのやり方は下の方に残しておきます。
動作
Instantiateとかはどうなんの?
試しにPrefabをInstantiateしてみましたが、Injectは発火しません。PlaceholderFactoryもありません(少なくとも現在は)。DiContainer.Injectのようなものもないようです。つまり自前でFactoryを作って、必要な依存をそこで解決しましょう。
所感
[Inject]の使い方にちょっと混乱しましたが、使えそうです。この辺りのZenject/Extenjectとの違いは軽量化のためでしょうか。
一つ興味深いのはZenject/ExtenjectにあるFactory関係の実装予定はないそうで。まぁ確かにあれはPlaceholderFactory作ってそれをSpawnerで包んで、なんか手間だなぁ、と感じたり。いまいち使いこなせてない感がありました。ですんで、多機能よりも軽量化に注力するのは大歓迎です。今後に期待です。
追記(7/2)
すみません。同じProjectにあったZenjectと競合してました。名前空間を貫通してくるとは思いませんでした。IInitialize関係がこちらの追記にあったのですが、上の方にスクリプト修正の上で転記しました。
追記(7/3)
ハダシAさんに質問した所、Zenject/Extenjectのような一方的なInjectは想定しておらず、Resolveが必要とのご回答をいただきました。Zenject/Extenjectのように使っていたInjectを上述のとおり修正いたしました。
追記(7/4)
0.0.2でRegisterComponentが実装されました。この記事をみて速攻で実装してくれました。熱い&感謝です。
IInitializableを使ってResolveする方法をこちらに残しておきます。以下のようなやり方は現在不要です。ただ、基本的には以下のようにしてInjectを発火させるんだ、と思っておいた方がVContainerを扱いやすくなると思います。
ちなみにAsとやっていますが、0.0.2で
builder.RegisterEntryPoint<Foo>(..);
が実装され、IInitializableとかITickableをまとめてやってくれるようになってます。
SayHelloMonoBehaviourにIInitializable等を実装
using UnityEngine;
using VContainer;
namespace VContainerSayHello
{
public class SayHelloMonoBehaviour : MonoBehaviour, IInitializable
{
[Inject]
public void Construct(SayHello sayHello)
{
}
public void Initialize()
{
}
}
}
using VContainer;
using VContainer.Unity;
namespace VContainerSayHello
{
public sealed class SayHelloInstaller : MonoInstaller
{
public override void Install(IContainerBuilder builder)
{
builder.Register<IPerson, Parent>(Lifetime.Singleton);
builder.Register<SayHello>(Lifetime.Singleton);
builder.RegisterComponentInHierarchy<SayHelloMonoBehaviour>().As<IInitializable>();
//HierarchyにあるSayHelloMonoBehaviourのInitializeを発火。Resolveされる。
}
}
}
IInitializableをMonoBehaviourに実装してAsで発火します。
何かのコンストラクタでResolveさせる
using UnityEngine;
using VContainer;
namespace VContainerSayHello
{
public class SayHelloMonoBehaviour : MonoBehaviour
{
[Inject]
public void Construct(SayHello sayHello)
{
}
}
}
using VContainer.Unity;
namespace VContainerSayHello
{
public class SayHelloMonoBehaviourInjector : IInitializable
{
public SayHelloMonoBehaviourInjector(SayHelloMonoBehaviour sayHelloMonoBehaviour)
{
}
public void Initialize()
{
}
}
}
using VContainer;
using VContainer.Unity;
namespace VContainerSayHello
{
public sealed class SayHelloInstaller : MonoInstaller
{
[SerializeField] private SayHelloMonoBehaviour sayHello = default;
public override void Install(IContainerBuilder builder)
{
builder.Register<IPerson, Parent>(Lifetime.Singleton);
builder.Register<SayHello>(Lifetime.Singleton);
builder.RegisterInstance(sayHello);
builder.Register<SayHelloMonoBehaviourInjector>(Lifetime.Singleton).As<IInitializable>();
//SayHelloMonoBehaviourInjectorがInitializeの発火でResolveされる。
//その際、コンストラクタ引数にSayHelloMonoBehaviourがあるのでsayHelloインスタンスがResolveされる。
//builder.RegisterEntryPoint<SayHelloMonoBehaviourInjector>(Lifetime.Singleton);
//で代用可能。
}
}
}
RegisterInstanceで該当のMonoBehaviourをRegisterして、適当なコンストラクタにぶち込みます。そのときResolveされます。
IInitializableをRegisterEntryPointやAsすることで、Zenject/ExtenjectのNonLazy的に扱えます。