IDisposableはどんな時に実装すればいいですか?


PRのコメントでIDisposableを実装してくださいと言われたが、どんな時に実装すればいいのかわからなかったので調べまとめました。
IDisposable実装の目的は参照の破棄なので、参照をいつ、どうやって破棄すべきかについて論じます。

IDisposableインターフェースとは

  • void Dispose()があるだけのシンプルなインターフェース。
  • ただのインターフェースであり、参照が切れた時にGC(ガベージコレクション)が走るなどということはない。

Dispose()はメモリを破棄するメソッドでありメモリーリークを防いだり、パフォーマンスを維持したりするために用いられる。
GCはマネージリソースを自動で破棄してくれる。
ただし、GCの破棄タイミングは予測できない上にアンマネージリソースについては破棄してくれないので開発者が管理する必要がある。
マネージリソース、アンマネージリソースの定義は文献によって微妙に差異があるのですが、下記記事で詳細調査が行われており大変参考になりました。

対象オブジェクト マネージ・アンマネージ
double[],string等(.NET組み込みライブラリ) マネージリソース
Stream,Bitmap等(派生クラスはOSリソースをラップしているのでアンマネージリソース) マネージリソース
(外部リソースの)ポインター等(ポインターそのものはマネージだが、ポインターが指すリソースはアンマネージ) マネージリソース
ポインターが指す外部リソース(C/C++、OSリソース) アンマネージリソース

OSリソース云々というのは下記のようなリソースを指します。

ファイルや周辺機器などのリソース(OSが管理している資源)を使用する場合、 まずリソースを使用する権利を取得し、 リソースに対する操作(ファイルの読み書きなど)を行った後、 リソース使用権を破棄する必要があります。

いつIDisposableを実装するか?

  • アンマネージリソースを扱う時 アンマネージリソースはGCが管理してくれないので必ず開発者が破棄処理を実装しなければなりません。 具体的に警戒しなければならないケースは下記
  • System.IO.Streamの派生クラスやSystem.Drawing.Imaging.Bitmap派生クラス、その他DB接続するためクラスなどのOSリソースをラップしたリソースを扱う時
    • C/C++などの.NETのメモリ管理対象外のライブラリを扱う時
  • マネージドリソースを扱う時 マネージドリソースを扱う場合も、GCに任せず任意のタイミングで破棄したい場合は開発者側で明示的に実装する場合があります。 例えば下記のようなケースが考えられます。
    • 画面遷移が多い場合
    • 長い処理(計算の為に巨大な配列を扱う、変換処理を繰り返すなど)を行う場合
    • こまめにメモリ管理をしてパフォーマンスを維持したい場合などです。
  • Observaberパターンを扱う時 購読解除する時に利用します。 ReactiveExtensionsを利用する場合は、もともとDisposeを実装してくれているのでDisposeするだけでOKです。 更に、まとめてDisposeするためのCompositeDisposableという型があるのでこれを利用します。

IDiposableをどう実装するか?

  • 基本Disposeパターン マネージドリソースだろうとアンマネージリソースだろうと一意な基本Disposeパターンをというものが存在します。 とりあえずこれを実装しておけばいい。

Disposeパターンは、ファイナライザーとIDisposableインターフェースの使用法と実装の標準化を意図したものです(.NETクラスライブラリ設計 9.4 Disposeパターン より)

public class DiposeableResourceHolder : IDisposable
{
    private SafeHandler _resouce; // リソースへのハンドル
    public DiposeableResourceHolder()
    {
        _resouce = ... // リソースへの割り当て
    }

    public void Dispose()
    {
        Dispose(true);
        GC.SuppressFinalize(this); // ファイナライザが不要なことをGCに伝える
    }

    protected virtual void Dispose(bool disposing) 
    {
        // disposingはIDisposable.Disposeなのかファイナライザーなのかを判断する
        if(disposing)
        {
            _resource?.Dispose();
        }
    }
}

あとはこれをDisposeが必要なクラスに継承させればよい。
追加処理(フィールドの購読解除など)を書く場合は、継承先でDispose(bool)をoverrideして追加処理を書けばよいのです。

突然出てきたファイナライザというのは簡単に言うとGCの実行です。
ファイナライザはGCにオブジェクトが回収された時(つまり参照が切れたタイミング)に実行される処理で、Object基底クラスに実装されたFinalize()メソッドが実行されることを指します。

  • Disposeパターンの拡張(ファイナライズ可能な型)

たとえファイナライザーが役に立つ可能性があるとしても、本当に、ファイナライザーは書かないほうがいいでしょう。....
型にファイナライザーを記述すると、たとえファイナライザーが全く呼び出されなかったとしても、その方を使うためのコストが大きくなります。
(.NETクラスライブラリ設計 9.4.2 ファイナライズ可能な型より)

アンマネージリソースの場合自前実装しなければならず、(前提状態を整えることが)非常に難しいそうで、この記事を読んでいる方々は一旦無視してもらっていいと思います。

知っておきたい方は下記記事の「Disposeの実装方法」を読んで頂ければと思います。

Disposeの処理が走るタイミング

さてDispose()メソッドに処理書いたはいいものの、これはどこで使えばいいのだろう?

  • 明示的に実行した時
    usingを使うことで明示的に実行できます。
    アンマネージリソースを利用した処理を行う場合はもちろん、上記の「いつIDisposableを実装するか?/マネージドリソースを扱う場合」で上げた例のようなケースで使用します。

    using( myConnection = new SqlConnection(connString))
    {
        myConnection.Open();
    }
    
    //上記はCLIにて下記に展開される
    try
    {
        myConnection = new SqlConnection(connString);
        myConnection.Open();
    }
    finally
    {
        myConnection.Dispose();
    }
    

まとめ

まとめると下記のようになります。

  • いつIDisposableを実装するか?
    • アンマネージリソースを扱う時
    • 重い、長い処理を行う時
    • 購読解除したい時
  • IDisposableをどう実装するか?
    • 基本Diposeパターンに従う

何がアンマネージリソースなのかマネージリソースなのか、又GCの対象やタイミングなど完全に理解していないですが

いつ警戒するか、どう実装するかがわかったので当面は生きていけそうです。