Disposeパターンを実装する

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

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

Dispose パターンですが、オブジェクトの使用を終了した時にリソースを解放する必要がある場合に適用できます。

「リソース解放」とは概ね以下のような操作を指します

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

こういうデータを扱うクラスには、IDisposable を実装してリソースを明示的に解放しましょうと言うのが基本の考え方です。

IDisposableを継承する意味と運用

IDisposable を継承する意味は、「このオブジェクトは解放する必要があるリソースを持ってますよ」、「だから使い終わったら解放してね」を外部に明示する役割があります。これがあると、「あ、解放する必要があるんだな」と思って解放処理を実装する使う側への設計メッセージになります。

イベントとか OS から借りてきたハンドルとか停止が必要なタイマー etc.. ありますが、最後に解放せずにオブジェクトを捨てるとリソースリークやメモリリークの原因となる何かを内部的に扱っていますよという感じです。

そこで何度も出てくる解放処理を簡単に実装できるのが using ステートメントです。

Dispose パターンを調べてでこのページに来る人に説明は不要かと思いますが、.NET の基本機能で IDisposable を継承したクラスは using ステートメントを使用したオブジェクトの有効期間を指定しつつスコープを抜ける時に Dispose メソッドの自動的に呼び出しができる機能があります。

public void Sample()
{
    using (SampleClass s = new SampleClass()) // SampleClassはIDisposableを継承している
    {
        // 1) ★このスコープを抜けると自動かつ安全にDisposeが呼び出される
    }

    // 2) ★C# 8.0以降は以下のように範囲を指定しないでも使用できる
    // オブジェクトのスコープを抜けるとDisposeが呼び出される
    using SampleClass s = new SampleClass();
}

どちらも finally ブロックで Dispose が null 後にl呼び出されるように展開されます。

// 以下のように展開される(イメージ)
SampleClass s;
try
{
    s = new();
}
finally
{
    s?.Dispose();
}

このため何度も同じ事をかかずに簡単かつ安全に利用することができます。

パターンを使わないDisposeの実装

何で 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;
    }
}
  • 各クラスでIDisposableを継承
  • 派生クラスは基底クラスのメソッドをbase.Disposeで呼び出す
  • 同じDisposeメソッドを定義することになるのでnewを使う

と、このように実装すると以下の問題が発生し、リソースが解放されないことがあります。

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

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

これで想定したリソースが解放されるいわゆるリソースリークが発生します。通常の業務でこうなると調査が非常に大変ですが、複数人で開発してると稀に事故が起きます。そもそも new でメソッドを派生クラスで再定義すること自体がアンチパターンなので避けたいところですが、、、

なのでこのような状況を避けるために Dispose パターンを使わないで IDisposable を実装するクラスは sealed 宣言して継承できないようにするなど何か仕組みが必要になったりします。

Disposeパターン

で、先述の派生クラス・基底クラスの扱いによる解放漏れを防ぎつつ、いい感じに解放処理をしてリソースを正しく解放しようというのが Dispose パターンとなります。

実装は概ね以下の通りです。

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

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

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

    // ファイナライザーはアンマネージリソースを保持するときだけ宣言する
    // ~Base() => this.Dispose(false); 
    // ファイナライザ経由の時はアンマネージリソースの解放だけ = false でDisposeを呼ぶ

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

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

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

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

// 派生クラス
public class Derived : Base
{
    // ファイナライザーはアンマネージリソースを保持するときだけ宣言する
    // ~Derived() => Dispose(false);

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

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

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

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

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

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

派生クラス側にも _dispose フラグを追加して、多重解放を避けるかどうかは状況に応じて追加したほうが良い場合もありますが今回は簡単のために基底クラスで一括でチェックとしています。

利用側のコード

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

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

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

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

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

もしこのコードを他人に配るような場合、公開メソッドには以下のように Dispose 済みだったら例外を投げる処理を実装していると破棄後して無効になったオブジェクトにアクセスしたというシグナルが明確になると思います(大抵実装間違いなので早期検出できる方が良い的な意味で

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

    // .NET7以降であれば以下が同じ意味で利用可能
    ObjectDisposedException.ThrowIf(_disposed, this);
}

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

注意点

コード中のコメントの通りですが、派生クラス側の override void Dispose(bool disposing) は派生クラスでも必要に応じて実装する必要があります。派生クラス側ではメソッドの最後に base.Dispose(disposing) を手書きする必要があります。忘れないようにしましょう。忘れるとリソース解放漏れが発生します ← 特に重要

ファイナライザー経由の呼び出しの時は Dispose(false) として、マネージリソースの解放をしない理由ですが、ファイナライザーが走っているときって、既に保有しているオブジェクトが(マネージリソースで親子関係があっても)先にGCされて存在しない可能性があり、破棄される順序が保障されていないのでオブジェクトに親子関係があっても子のほうが先に破棄されてたりする未定義動作ゾーンなのでファイナライザー中にマネージリソースを触らないようにするという .NET のお約束のためです。

参考資料

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

MSDNの「Diposeパターン」

https://learn.microsoft.com/ja-jp/dotnet/standard/design-guidelines/dispose-pattern

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

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