WebSurfer's Home

Filter by APML

Dispose パターン

by WebSurfer 31. May 2019 18:30

Visual Studio Community 2015(以下、VS2015 と書きます)には Dispose パターンに準拠したコードの骨組みを自動生成する機能が追加されています。

2026/08/23 追記: Visual Studio 2026 でも調べてみましたが、同様な機能が実装されています。自動生成されるコードが少々異なりますが、基本的に同じです。

Dispose パターンの実装

上の画像がそれで、IDisposable とタイプすると電球マークが表示されるので、その横の▼印をクリックすると Disposeパターンを選択できるようになります。

ここで、[Dispose パターンを使ってインターフェイスを実装します]にマウスをポイントすると以下の画像(Dispose パターンの骨組みのコード)が表示され、クリックするとそのコードがクラスに挿入されます。

([... 明示的に ...]の方を選ぶと、Dispose メソッドが IDisposable.Dispose と明示的に実装されます。その場合、クラスインスタンスから Dispose には直接アクセスできませんので注意してください。明示的の使い道は調べてなくて不明です)

コードの実装

そういう機能が追加されていることを知ってました? 実は自分は最近まで知らなかったです。(汗) VS2010 にはその機能はありませんでしたので、VS2012 ~ VS2015 のどれかから追加されたようです。

Dispose パターンの実装については、Microsoft のドキュメント「Dispose メソッドの実装」などに書かれていますが、読んでもよく分かりませんでした。(笑)

(ちなみに、VS2015 で自動生成されるのは上の Microsoft のドキュメントの「Dispose パターンには 2 種類あります」以下に書いてある後者の Object.Finalize メソッドをオーバーライドする方です)

VS2015 で自動生成されたコードと、ネットの情報「確保したリソースを忘れずに解放するには?」や「C# のファイナライザ、Dispose() メソッド、IDisposable インターフェースについて」などを読んで、やっと Dispose パターンを理解できたような気がします。

Dispose パターンの実装が必要なのは、アンマネージドリソースを使用するクラスのみです。アンマネージドリソースの種類で一般的なのは、ファイル、ウィンドウ、ネットワーク接続、データベース接続などのオペレーティングシステムリソースをラップしたオブジェクトです。

.NET Framework のクラスライブラリの中にも、例えば FileStream のようにファイルを開くために Windows API を呼び出してファイルハンドルを保持するというように、アンマネージドリソースを利用するものがあります。

そういうクラスは、IDisposable インターフェイスを継承して Dispose パターンを使った実装がされていて、Dispose メソッドでアンマネージドリソースを開放できるようになっているはずです。

ソースを見たわけではないので、.NET Framework のクラスライブラリの全部が全部そうなのかは分かりませんが、Dispose メソッドを実装しているクラスは内部でアンマネージドリソースを使っていると考えて、そのオブジェクトが使用されなくなった時点で Dispose メソッドを呼び出すことを基本とすべきと思います。

自分で作るカスタムクラスの場合、マネージドリソースしか保持しない場合は Dispose パターンの実装は不要です。必要なのはアンマネージドリソースを保持する場合のみですが、それには以下のケースがあると思います。

  1. Dispose パターンを実装した .NET のクラスのインスタンスを保持している。
  2. クラス内でアンマネージドリソースを取得し、それを保持している。  

VS2015 のウィザードが生成する Dispose パターンの骨組みのコードを利用して、上記 1 および上記 1 + 2 のケースでコードを実装してみます。

上記 1 のケース

Dispose パターンを実装した .NET のクラスの例として FileStream を使いました。

public class DisposableSample : IDisposable
{
    private FileStream? _fs; // マネージドリソース
    private bool _disposed;  // 重複呼出の検出用

    public DisposableSample(string path)
    {
        // マネージドリソースの初期化
        _fs = File.OpenRead(path);
    }

    public void Dispose()
    {
        Dispose(true);
    }

    protected virtual void Dispose(bool disposing)
    {
        if (_disposed) return;
        if (disposing)
        {
            // マネージドリソースの解放
            _fs?.Dispose();
            _fs = null;
        }
        
        _disposed = true;
    }
}

上記 1 + 2 のケース

アンマネージド(GC では解放できない)リソースとして Marshal.AllocHGlobal メソッドでプロセスのアンマネージドメモリから割り当てたメモリを例に使います。このメモリは、Marshal.FreeHGlobal メソッドを使用して解放する必要があります。

public class DisposableSample : IDisposable
{
    private FileStream? _fs;  // マネージドリソース    
    private IntPtr _nativeHandle = IntPtr.Zero;  // アンマネージドリソース
    private bool _disposed;   // 重複呼出の検出用


    public DisposableSample(string path)
    {
        // マネージドリソースの初期化
        _fs = File.OpenRead(path);

        // アンマネージドメモリからメモリを割り当て
        _nativeHandle = NativeHelpers.AcquireNativeResource();
    }

    public void Dispose()
    {
        Dispose(true);

        // Dispose() が呼ばれ、上の Dispose(true) が実行された
        // 場合は Dispose(true) 内でアンマネージドリソースは解
        // 放されるので、ファイナライザが実行されないようにする
        GC.SuppressFinalize(this);
    }

    protected virtual void Dispose(bool disposing)
    {
        if (_disposed) return;
        if (disposing)
        {
            // マネージドリソースの解放
            _fs?.Dispose();
            _fs = null;
        }

        // アンマネージドリソースの解放
        if (_nativeHandle != IntPtr.Zero)
        {
            NativeHelpers.ReleaseNativeResource(_nativeHandle);
            _nativeHandle = IntPtr.Zero;
        }

        _disposed = true;
    }

    // ファイナライザをオーバーライド
    // Dispose し忘れても、ガベージコレクタが働いたときにファ
    // イナライザが呼ばれるので、そこでアンマネージドリソース
    // を解放するよう以下のコードを実装する。
    ~DisposableSample()
    {
        Dispose(false);
    }
}

上のコードにある、アンマネージドリソースを取得・開放する AcquireNativeResource メソッドと ReleaseNativeResource メソッドの実装例を参考までに載せておきます。

internal static class NativeHelpers
{
    // Acquire a native buffer (example: allocate N bytes)
    public static IntPtr AcquireNativeResource(int size = 256)
    {
        if (size <= 0) throw new ArgumentOutOfRangeException(nameof(size));
        IntPtr handle = Marshal.AllocHGlobal(size);

        // Initialize memory to zero (optional, clear previous contents)
        for (int i = 0; i < size; i++)
        {
            Marshal.WriteByte(handle, i, 0);
        }

        return handle;
    }

    // Release the native buffer
    public static void ReleaseNativeResource(IntPtr handle)
    {
        if (handle == IntPtr.Zero) return;
        Marshal.FreeHGlobal(handle);
    }
}

プログラマが Dispose() するコードを書くのを忘れた場合でもガベージコレクタは働きます。ガベージコレクタではアンマネージドリソースは開放できませんがファイナライザは呼び出されるそうです。なので、上の ~DisposableSample() でファイナライザをオーバーライドし、その中で Dispose(false) を呼び出してアンマネージドリソースを開放するというパターンを実装するそうです。

Dispose() メソッドが呼ばれ、それから Dispose(bool disposing) メソッドが呼ばれてアンマネージドリソースを解放した場合は GC.SuppressFinalize(this) を呼び出します。 ファイナライザの実行はパフォーマンスに大きな影響を与えるそうですが、GC.SuppressFinalize メソッドを呼び出しておくと、ファイナライザの呼び出しは行われなくなります。


Dispose パターンを実装済のクラスを継承したカスタムクラスを作成し、そのカスタムクラスに Dispose パターンを実装済のクラスをメンバーに含める場合、メンバーに含めたクラスを Dispose するため、カスタムクラスにどのようなコードを書けばよいかについては、別の記事「Dispose パターン (その 2)」に書きます。

Tags: , , ,

.NET Framework

About this blog

2010年5月にこのブログを立ち上げました。主に ASP.NET Web アプリ関係の記事です。ブログ2はそれ以外の日々の出来事などのトピックスになっています。

Calendar

<<  October 2026  >>
MoTuWeThFrSaSu
2829301234
567891011
12131415161718
19202122232425
2627282930311
2345678

View posts in large calendar