thinva
@thinva

Are you sure you want to delete the question?

Leaving a resolved question undeleted may help others!

IDisposable継承クラスのDIコンテナ登録

IDisposable を継承したクラスを DIコンテナ に登録している状況で、取得したインスタンスの Dispose をどのように行うかで悩んでいます。

ここでは UnityContainer を前提に話を進めますが、他のDIライブラリでのソリューションがありましたら ご教示頂きたく思います。

自分の認識

自分の認識をまとめてみました。認識誤りがありましたらご教示ください。 これらの詳細は別途 Qiita の記事フィールドにまとめています。

Prism.Unity のライフサイクル調査

  1. DIコンテナのライフサイクルが PerContainer(Singleton) や Hierarchical (Scoped) の場合、DIコンテナが生成インスタンスの参照を保持している。
    DIコンテナはそれ自体が Dispose されるときに、参照保持インスタンスを Dispose してくれる。

  2. DIコンテナのライフサイクルが Transient の場合、DIコンテナはインスタンスの生成後に関与しない。(参照を保持しないので Dispose もしてくれない)
    なので、Transient で登録されたインスタンスが IDisposable を継承している場合、インスタンスを取得したユーザが Dispose する責任を負う。

本題の悩み

ここまでの認識を踏まえますと、以下になると思います。

DIコンテナ からインスタンスを取得したユーザは、取得インスタンスが DIコンテナ内 でどのようなライフサイクルで管理されているかを認識して管理しなければならない。
- IDisposable を継承している Transient 管理のインスタンスを取得した場合 は、自分で Dispose しなければならない。
- IDisposable を継承していても Singleton 管理のインスタンスは(他も参照してるので)Dispose してはならない。

これだと、ユーザはインスタンスの管理を DIコンテナ にお任せしているにも関わらず、DIコンテナ のインスタンス管理状況を知る必要があり、依存関係に違和感を感じます。

DIコンテナのユーザが、この程度の管理責任を被るのは 仕方ないものなのでしょうか?

UnityContainer での補足

他のDIライブラリのことが分からないので、UnityContainer に限定して補足いたします。

  1. UnityContainer であれば、ユーザ側もDIコンテナから管理されている型のライフサイクル(参照の保持有無)を取得できますので、その結果に応じて ええ感じ に管理することも可能です。(手間ですが…)

  2. UnityContainer には、Transient だけど参照保持するライフサイクル(PerContainerTransient)が用意されていますので、そちらを使うことでユーザの Dispose 責任を解消できます。
    ですが、Transient が存在している以上、開発中に ライフサイクル が変更される可能性もありますので、そこまで考慮すると結局 ユーザ側で DIコンテナ の管理状況を調べて対応する しか無くなってします…

参考

以下に本悩みと同じ内容が挙がっていますが、回答は付いていません。

Disposing needed in Unity? - stackoverflow

When you register types that implement the IDisposable interface using the TransientLifetimeManager (you get a new instances each type you call Resolve on the container), it is up to you to call Dispose on the instance.

Transient の場合、Dispose を呼ぶのはユーザの責任です。

manually calling Dispose means you have to be sure that it's been registered with a TransientLifetimeManager

ユーザが手動で Dispose を呼ぶには、(取得インスタンスが)Transient で登録されていることを確認する必要がある。

終わりに

趣味プログラマ なので知り合いに詳しい方がおらず、的外れなことを質問しちゃっているかもしれません…

0

4Answer

ユーザ側で DIコンテナ の管理状況を調べて対応する

これはやるべきではないことなので、別の方策を考えた方がよいでしょう。

また、使う側が気を付けるようなやり方は遅かれ早かれ破綻することが目に見えているためお勧めできません。

これ以外の方策としてとれる方針は、すべてのIDisposableなインスタンスをsingletonに寄せるかtransientに寄せるかのいずれかになるかと思います。

singleton -> transient寄せの場合

同一インターフェースでラッパークラスを作り、IDisposable.Dispose()では何もしない。
この場合、singletonなオブジェクトのIDisposable.Dispose()で込み入ったことをしようとすると何もできなくなります。
回避策として、singletonな生オブジェクトに対して、Keyコンテキスト付きでバインドしておき、DIコンテナの終了でいい感じに破棄させるとかかな?

transient->singleton寄せの場合

IDisposableなオブジェクトを直接バインドするのではなく、IDisposableなオブジェクトを生成するファクトリを代わりにバインドし、このファクトリを介して実際のオブジェクトの作成を委譲する。
そして作成したオブジェクトの破棄を使う側に押し付ける(ここは元々transientなので変わらず)。
イメージとしては、System.IO.File.OpenStreamを作ることと類似しています。
また、このファクトリはIDisposableにはしない(直接破棄していた箇所がことごとくコンパイルエラーになるため、直し漏れを防げる)。

1Like

Comments

  1. @thinva

    Questioner

    ご回答ありがとうございます!
    コメントではマークダウンが効かなさそうだったので以下に投稿しました。

一部理解できなかったのですが(Keyコンテキスト付きバインド)

これはどのように伝えたらよいか迷った名称で、一般的に使われる用語ではないです(だったら補足しろよって話でしょうが)

Javaになってしまいますが例えば、Google Guiceでは、インターフェースでバインドする機能に加えて、インターフェース+Keyでバンドする機能があります。

この機能は、同一インタフェースではあるが、場面(コンテキスト)に応じて具体的なインスタンス変えて注入する場合に使用します。

DIコンテナによって、必ずしもサポートしているとは限らないかもしれないです(その場面に応じて使い分ける別機能があるのかも)

1Like

Comments

  1. @thinva

    Questioner

    補足ありがとうございます! おかげさまで 1件目の回答の理解が進みました!
    質問でも例に挙げさせて頂いている C# の UnityContainer でも GoogleGuice の Key に相当する機能が存在しております。(こちらでは Name と呼ばれています)

ここまで議論を積み重ねてきてなんですが、そもそもIDisposableなオブジェクトを直接注入すべきは、いちどちゃんと考えた方がよいかもしれません。

上述のラッパークラスに似たシチュエーションですが、IDisposableなリソースの破棄はラッパークラスに完全に任せ、ラッパークラスはIDisposableにしないというのも一つの考えかと思います。

例えば、FooStreamを直接バインドするのではなく、FooServiceのようなサービスをバインドするような。

具体的には

void WriteLine(string text) { ... } // メソッドの中でリソースをOpen/Close

もしくは、複数行になるなら、

void WriteLines(params string[] texts) { 
    // メソッドの中でリソースusingを制御
    ... 
} 

とか、

void WriteLines(IEnumerable<string> texts) {
    // メソッドの中でリソースusingを制御
    ... 
} 

改行やFlushを細かく制御したいためにどうしても生Streamが必要なら

void WriteLine(Action<Stram> callback) { 
    // メソッドの中でリソースusingを制御
    ... 
} 

としておく。コールバックから渡されるStream引数をDisposeすることはもちろん行わない

1Like

Comments

  1. @thinva

    Questioner

    追加の回答ありがとうございます!

    TextFileWriter のような Stream を 必要なタイミングだけ Open に(定常時は Close状態に)することで、プログラム終了時の File.Close() を不要にする(IDisposable を撲滅させる)で理解しました。

    DIコンテナで IDisposable を扱う際の 定石 がなさそうなので、このような設計も有効かと思いました。ありがとうございました!

@ktz_aliasさん ご回答ありがとうございます🙇‍♂️

使用者が個別に対応するより、登録者がライフサイクルを統一して管理するべき で理解いたしました!

私の知識不足で一部理解できなかったのですが(Keyコンテキスト付きバインド)、理解した内容をコードに書いてみました。

singleton -> transient 寄せ

singleton -> transient 寄せのコード / .NET Fiddle

`singleton` -> `transient` 寄せのコード全文
using System;
using Unity;

using var container = new UnityContainer();
container.RegisterType<IService, Service>();

var s1 = container.Resolve<IService>();
s1.DoSomething();
if (s1 is IDisposable d) d.Dispose();

/* 実行結果
 *  Service.DoSomething()
 *  ServiceWrapper.Dispose()
 */

interface IService
{
    void DoSomething();
}

// 同一インターフェイスのラッパークラス
abstract class ServiceWrapper : IService, IDisposable
{
    public virtual void DoSomething() => throw new NotImplementedException();
    public void Dispose() => Console.WriteLine("ServiceWrapper.Dispose()");
}

class Service : ServiceWrapper
{
    public override void DoSomething() => Console.WriteLine("Service.DoSomething()");
    public void Dispose()
    {
        // これは実装しないルール。誤って実装しても new を付けない限りは有効にならない
        //Console.WriteLine("Service.Dispose()");
    }
}

transient -> singleton 寄せ

transient -> singleton 寄せのコード / .NET Fiddle

`singleton` -> `transient` 寄せのコード全文
using System;
using Unity;

using var container = new UnityContainer();
container.RegisterType<ServiceFactory>(TypeLifetime.Singleton);

var factory = container.Resolve<ServiceFactory>();

var s1 = factory.CreateService();
s1.DoSomething();
if (s1 is IDisposable d) d.Dispose();


/* 実行結果
 *  Service.DoSomething()
 *  ServiceWrapper.Dispose()
 */


interface IService
{
    void DoSomething();
}

class Service : IService, IDisposable
{
    public void DoSomething() => Console.WriteLine("Service.DoSomething()");
    public void Dispose() => Console.WriteLine("Service.Dispose()");
}

class ServiceFactory //: IDisposable にしない!
{
    public IService CreateService() => new Service();
}
0Like

Your answer might help someone💌