【Unity】MicrosoftによるDIの基礎実装Microsoft.Extensions.DependencyInjectionをUnityで使う

UnityにMicrosoft.Extensions.DependencyInjectionを導入する方法と、ServiceCollectionの基本的な使い方についてまとめました。

Unity2020.1.10 (Api Compatibility Level : .NET Standard 2.0)

はじめに

本記事ではUnity(ゲームエンジンの方)にMicrosoft.Extensions.DependencyInjectionを導入する方法とその使い方についてまとめます。

www.nuget.org

UnityにはVContainer(下記)のような素晴らしいDIライブラリがあるので実際にこれを使う機会はないかもしれませんが、
DIのコアな部分のみの実装となるためDIの基本的なコンセプトと使い方を学ぶためには良いと思います。

light11.hatenadiary.com

なおDIコンテナにもUnityというライブラリが存在して文脈的にややこしいですが、本記事に出てくるUnityはゲームエンジンのUnityです。

インストール

さてそれではまずMicrosoft.Extensions.DependencyInjectionをインストールします。

まずNuGetでMicrosoft.Extensions.DependencyInjectionとその依存関係を取得します。
ここについては詳細は割愛しますが、必要に応じて以下の記事の「NuGetでインストールする」あたりを参考にしてください。

light11.hatenadiary.com

次に以下の5つの.NET Standard 2.0用のdllをUnityにインポートします。

  • Microsoft.Bcl.AsyncInterfaces
  • Microsoft.Extensions.DependencyInjection.Abstractions
  • Microsoft.Extensions.DependencyInjection
  • System.Runtime.CompilerServices.Unsafe
  • System.Threading.Tasks.Extensions

これでUnityでServiceCollectionを使えるようになりました。

基本的な使い方

まず基本的な実装例を示します。

using Microsoft.Extensions.DependencyInjection;
using UnityEngine;

public class Example : MonoBehaviour
{
    private ServiceProvider _serviceProvider;
        
    private void Start()
    {
        // サービスを登録
        var services = new ServiceCollection();
        services.AddSingleton<IFooService, FooService>();
        
        // サービスプロバイダーをビルド
        var provider = services.BuildServiceProvider();
        _serviceProvider = provider;
        
        // サービスを取得(登録されてなかったらnull)
        var service = provider.GetService<IFooService>();
        // サービスを取得(登録されてなかったらException)
        //var service = provider.GetRequiredService<IFooService>();
    }

    private void OnDestroy()
    {
        // 全てのサービスのDisposeが呼ばれる
        _serviceProvider.Dispose();
    }
}

使い方としては上記の通りインターフェースとその実装クラスの型をServiceCollectionにAddして、それをサービスプロバイダーから取得するだけです。
いくつかのサービスをAddしておけば、サービス間の依存は自動的に注入されます。

var services = new ServiceCollection();
// FooServiceはコンストラクタ引数にIBarServiceを取るとする
services.AddSingleton<IFooService, FooService>();
services.AddSingleton<IBarService, BarService>();

var provider = services.BuildServiceProvider();
_serviceProvider = provider;

// IBarServiceが注入されたFooServiceを得られる
var service = provider.GetService<IFooService>();

サービスの有効期間

ServiceCollectionに登録されたサービスには有効期間があります。

まず前節のようにAddSingleton()により登録されたサービスは、何度GetService()しても同じインスタンスを返します。

using Microsoft.Extensions.DependencyInjection;
using UnityEngine;

public class Example : MonoBehaviour
{
    private ServiceProvider _serviceProvider;
        
    private void Start()
    {
        var services = new ServiceCollection();
        services.AddSingleton<IFooService, FooService>();

        var provider = services.BuildServiceProvider();
        _serviceProvider = provider;
        
        // AddSingletonは何度Getしても同じインスタンスを返す
        var service1 = provider.GetService<IFooService>();
        var service2 = provider.GetService<IFooService>();
        Debug.Log(service1.Equals(service2)); // True
    }

    private void OnDestroy()
    {
        _serviceProvider.Dispose();
    }
}

次に、以下のようにAddTransient()でサービスを登録するとGetするたびに違うインスタンスを返します。

using Microsoft.Extensions.DependencyInjection;
using UnityEngine;

public class Example : MonoBehaviour
{
    private ServiceProvider _serviceProvider;
        
    private void Start()
    {
        var services = new ServiceCollection();
        services.AddTransient<IFooService, FooService>();

        var provider = services.BuildServiceProvider();
        _serviceProvider = provider;
        
        // AddTransientはGetするたびに違うインスタンスを返す
        var service1 = provider.GetService<IFooService>();
        var service2 = provider.GetService<IFooService>();
        Debug.Log(service1.Equals(service2)); // False
    }

    private void OnDestroy()
    {
        _serviceProvider.Dispose();
    }
}

最後にAddScoped()です。
これは自身で定義したスコープ内では常に同じインスタンスを返し、
スコープを抜けると別のインスタンスを返します。

スコープは以下のようにIServiceProvider.CreateScope()で作ります。

using Microsoft.Extensions.DependencyInjection;
using UnityEngine;

public class Example : MonoBehaviour
{
    private ServiceProvider _serviceProvider;
        
    private void Start()
    {
        var services = new ServiceCollection();
        services.AddScoped<IFooService, FooService>();

        var provider = services.BuildServiceProvider();
        _serviceProvider = provider;

        IFooService service1;
        IFooService service2;
        IFooService service3;
        IFooService service4;
        using (var serviceScope = provider.CreateScope())
        {
            // 同じスコープ内では同じインスタンスを返す
            service1 = serviceScope.ServiceProvider.GetService<IFooService>();
            service2 = serviceScope.ServiceProvider.GetService<IFooService>();
        }
        using (var serviceScope = provider.CreateScope())
        {
            // スコープが変わると違うインスタンスを返す
            service3 = serviceScope.ServiceProvider.GetService<IFooService>();
        }
        service4 = provider.GetService<IFooService>();
        Debug.Log(service1.Equals(service2)); // True
        Debug.Log(service1.Equals(service3)); // False
        Debug.Log(service3.Equals(service4)); // False
    }

    private void OnDestroy()
    {
        _serviceProvider.Dispose();
    }
}

参考

docs.microsoft.com