Disposeパターンを実装する

Disposeパターンパターンですが最近は IDE で クラスが IDisposable の継承を検出すると自動的に Dispose パターンをコードに追加してくれるので具体的な書き方は完璧に覚えている必要が無いですが、Disposeパターンについて調べたので結果をまとめてみようと思います。

VisualStudio だと Dispose パターンが「破棄パターン」と表示されますが同じことを指しています。

Disposeパターンですが、クラスで終了時にリソースを開放す必要がある場合に適用できます。

「リソース解放」とはまぁだいたい以下のような行為を指します

  • イベントの開放
  • 大きいマネージオブジェクトに null を代入する行為
  • IDisposable を継承するメンバー変数の Dispose 呼び出し
  • マネージリソースの明示的な開放(ファイルハンドル、リストのクリア)
  • アンマネージリソース(メモリ、ハンドル、外部リソースの解放)

こういう処理をしているクラスにはまず IDisposable を実装したほうがいいです。

普通のDisposeの実装

普通に実装すると以下のようになります。

// 通常のDisposeの実装
public class Sample : IDisposable
{
    bool _disposed = false;
    public void Dispose()
    {
        if(_disposed) return;
        // リソース解放処理
        _disposed = true;
    }
}

// 派生クラスがある場合
public class Foo : Sample, IDisposable
{
    bool _disposed = false;
    public new void Dispose() // FooもIDisposableなのでメソッドを再定義する
    {
        base.Dispose(); // まず基底クラスをDisposeする

        if(_disposed) return;
        // 自分のリソース解放処理
        _disposed = true;
    }
}

まぁでもこのコードって解放されないことがあるんですよね、、、

var sample = new Foo();
Sample foo = sample; // 基底クラスにアップキャスト

foo.Dispose(); // ★★★FooクラスのDisposeが呼ばれない

これで解放漏れでリソースリークして最後すごい大変な目に遭うのは複数人で開発してるとありがちです。

なので Disposeパターンを使わないで IDispose を実装するクラスは sealed 宣言して継承できないようにしたほうが安全かもしれません。

Disposeパターン

で、先述の解放漏れをフォローして解放漏れを予防しようというのが Dispose パターンです。

実装はだいたい以下のような形式です。

public class Base : IDisposable
{
    // Disposeされたかどうかを表すフラグ
    //   true : Dispose済み
    //   false : Disposeまだ
    bool _disposed;

    // IDisposableの実装
    public void Dispose()
    {
        Dispose(true);

        // 解放済みなのでファイナライザーからの呼び出しを抑制する
        GC.SuppressFinalize(this);
    }

    ~Base() => this.Dispose(false); // ファイナライザ経由の時はアンマネージリソースの開放だけ

    // Disposeパターン用のメソッド
    //
    // disposing
    //   true : Disposeメソッドからの呼び出し
    //   false : デストラクタからの呼び出し
    protected virtual void Dispose(bool disposing)
    {
        if (_disposed)
        {
            return; // 解放済みなので処理しない、最基底クラスは早期リターンでOK
        }

        if (disposing)
        {
            // マネージリソースの解放処理を記述
        }

        // アンマネージリソースの解放処理を記述

        _disposed = true; // Dispose済みを記録
    }
}

// 派生クラス
public class Derived : Base
{
    // Disposeされたかどうかを表すフラグ
    bool _disposed;

    ~Derived() => Dispose(false);

    // Baseクラスのメソッドをoverrideしている
    protected override void Dispose(bool disposing)
    {
        // 基底クラスのつくりと同じ
        if (!_disposed)
        {
            if (disposing)
            {
                // マネージリソースの解放処理を記述
            }

            // アンマネージリソースの解放処理を記述

            _disposed = true; // Dispose済みを記録
        }

        // ★★★基底クラスの Dispose を呼び出す(忘れないように!)
        base.Dispose(disposing);
    }
}

冒頭のコードと違うところは、以下のように呼び出される状態に依って解放漏れが無くなります。

var d = new Derived();
Base b = d;

b.Dispose(); // ちゃんと全部開放される

利用側のコード

上記コードの使用した場合のコード例となります。

public class AppMain
{
    public void Main(string[] args)
    {
        // ☆使用例1☆
        Base base_1 = new Derived();
        base_1.Dispose();
        // Derived.Dsipose(bool) → Base.Dispose(bool) の順に呼ばれる

        // ☆使用例2☆
        using(Base base_2 = new Derived())
        {
            // (省略)
        }
        // この時点で base_1 の Dipose と同じ動きになる

        // ☆使用例3☆
        Derived derived = new Derived();
        ((Base)derived).Dipose();
        // これも Derived.Dsipose(bool) → Base.Dispose(bool) の順に呼ばれる

        // ☆使用例4☆
        using(Derived  base_2 = new Derived())
        {
            // (省略)
        }
        // これも同じ
    }
}

もしこのコードを他人に配るような場合、公開メソッドには以下のようにDipose済みだったら例外を投げる処理を実装をするのが一般的です。

public void Foo()
{
    if(this.disposed)
    {
        throw new InvaliedOperationException("Object is disposed.");
    }
}

ただし関係するプロパティとメソッド冒頭にこの処理を入れると実装は結構面倒です…

注意点

コード中のコメントの通りですが、Dipose(bool disposing)は各々のクラスで実装する必要があり、特に派生クラス側は IDE の補完が効かないことが多いので、手書きする必要がありますが、実装を間違えないようにしましょう。特に base.Dispose(bool) の呼び出し忘れしないようにしましょう。

各クラスには IDisposable は付けても付けなくても良いです。ただし、明示したほうが分かりやすいので自分は Dispose メソッドは実装しませんが IDisposable インターフェースは各クラスに継承しています。

参考資料

以下資料を参考に書きました。

読まないとモグリ、Effective C#

MSDNの「Diposeパターン」

https://msdn.microsoft.com/ja-jp/library/b1yfkh5e.aspx

Dispose にまつわる余談 - ++C++飛行

https://ufcpp.net/study/csharp/rm_disposable.html