Dispose パターン

Note

このコンテンツは、Pearson Education, Inc. の許可を得て、『Framework Design Guidelines: Conventions, Idioms, and Patterns for Reusable .NET Libraries, 2nd Edition (フレームワーク設計ガイドライン: 再利用可能な .NET ライブラリの規約、表現形式、およびパターン、第 2 版)』から転載されています。 この版は 2008 年に出版され、その後、この本は第 3 版で全面的に改訂されました。 このページの情報の一部は古くなっている可能性があります。

すべてのプログラムは、実行中に 1 つ以上のシステム リソース (メモリ、システム ハンドル、データベース接続など) を取得します。 開発者は、このようなシステム リソースを使用する場合、取得して使用した後に解放する必要があるため、注意する必要があります。

CLR は、自動メモリ管理のサポートを提供します。 マネージド メモリ (C# 演算子 new を使用して割り当てられたメモリ) は、明示的に解放する必要はありません。 ガベージ コレクター (GC) によって自動的に解放されます。 これにより、開発者はメモリの解放という面倒で困難な作業から解放されます。また、これが .NET Framework によって提供される前例のない生産性の主な理由の 1 つです。

残念ながら、マネージド メモリは、多くの種類のシステム リソースの 1 つに過ぎません。 マネージド メモリ以外のリソースは引き続き明示的に解放する必要があり、アンマネージド リソースと呼ばれます。 GC は、特にこのようなアンマネージド リソースを管理するようには設計されていません。つまり、アンマネージド リソースを管理する責任は開発者の手中にあります。

CLR は、アンマネージド リソースの解放をいくつか手助けします。 System.Object は、オブジェクトのメモリが GC によって再利用される前に GC から呼び出される仮想メソッド Finalize (ファイナライザーとも呼ばれます) を宣言し、アンマネージド リソースを解放するためにオーバーライドすることができます。 ファイナライザーをオーバーライドする型は、ファイナライズ可能型と呼ばれます。

一部のクリーンアップ シナリオではファイナライザーが効果的ですが、次の 2 つの大きな欠点があります。

  • あるオブジェクトがコレクションの対象として GC に検出されると、ファイナライザーが呼び出されます。 これは、リソースが不要になった後、ある程度の不確定な期間に発生します。 開発者がリソースを解放できる場合、またはリソースがファイナライザーによって実際に解放されるまでの延期時間は、少ないリソース (簡単に使い果たすことができるリソース) を多く取得するプログラムや、リソースの使用を維持するためにコストがかかる場合 (たとえば、大規模なアンマネージド メモリ バッファー) では受け入れられない場合があります。

  • CLR がファイナライザーを呼び出す必要がある場合は、オブジェクトのメモリのコレクションをガベージ コレクションの次のラウンドまで延期する必要があります (ファイナライザーはコレクション間で実行されます)。 これは、オブジェクトのメモリ (およびそれが参照するすべてのオブジェクト) が長期間解放されないことを意味します。

そのため、できるだけ早くアンマネージド リソースを再利用することが重要な場合や、不足しているリソースを扱う場合、またはファイナライズの追加された GC オーバーヘッドが許容されない極めて効率の良いシナリオでは、ファイナライザーのみに依存することは、多くのシナリオで適さないことがあります。

.NET Framework には、System.IDisposable インターフェイスが用意されています。これは、アンマネージド リソースが必要ではなくなったらすぐに解放する手動の方法を開発者に提供するために実装する必要があります。 また、オブジェクトが手動で破棄され、ファイナライズを行う必要がなくなったことを GC に伝えられる GC.SuppressFinalize メソッドも提供されます。その場合、オブジェクトのメモリは早期に再利用されます。 IDisposable インターフェイスを実装する型は、破棄可能型と呼ばれます。

破棄パターンは、ファイナライザーと IDisposable インターフェイスの使用方法と実装を標準化することを目的としています。

パターンの主な動機は、Finalize および Dispose メソッドの実装の複雑さを軽減することです。 複雑さは、メソッドがコード パスのすべてではなく一部を共有しているという事実により生じています (違いについては、この章で後述します)。 さらに、決定論的リソース管理の言語サポートの進化に関連するパターンの一部の要素には、歴史的な理由があります。

破棄可能型のインスタンスを含む型に基本破棄パターンを実装します。 基本パターンについて詳しくは、「基本破棄パターン」セクションをご覧ください。

ある型が他の破棄可能なオブジェクトの有効期間の要因となっている場合は、開発者にはそれらを破棄する方法も必要です。 コンテナーの Dispose メソッドを使用することが、これを可能にする便利な方法です。

明示的に解放する必要があり、ファイナライザーがないリソースを保持する型に、基本破棄パターンを実装し、ファイナライザーを提供します。

たとえば、アンマネージド メモリ バッファーを保存する型にパターンを実装する必要があります。 「ファイナライズ可能型」セクションでは、ファイナライザーの実装に関連するガイドラインについて説明します。

アンマネージド リソースまたは破棄可能なオブジェクトは保持しないが、サブタイプは保持している可能性が高いクラスに、基本破棄パターンを実装することを検討してください。

この優れた例が、System.IO.Stream クラスです。 これは、リソースを保持しない抽象基底クラスですが、そのサブクラスのほとんどは保持しているため、このパターンを実装しています。

基本破棄パターン

パターンの基本的な実装では、System.IDisposable インターフェイスを実装し、Dispose メソッドとオプションのファイナライザーの間で共有されるすべてのリソース クリーンアップ ロジックを実装する Dispose(bool) メソッドを宣言します。

次の例は、この基本パターンの簡単な実装を示しています。

public class DisposableResourceHolder : IDisposable {

    private SafeHandle resource; // handle to a resource

    public DisposableResourceHolder() {
        this.resource = ... // allocates the resource
    }

    public void Dispose() {
        Dispose(true);
        GC.SuppressFinalize(this);
    }

    protected virtual void Dispose(bool disposing) {
        if (disposing) {
            if (resource!= null) resource.Dispose();
        }
    }
}

ブール値パラメーター disposing は、メソッドが IDisposable.Dispose 実装から呼び出されたか、ファイナライザーから呼び出されたかを示します。 Dispose(bool) 実装では、他の参照オブジェクト (前のサンプルのリソース フィールドなど) にアクセスする前に、パラメーターを確認する必要があります。 このようなオブジェクトには、メソッドが IDisposable.Dispose 実装から呼び出された場合 (disposing パラメーターが true の場合) にのみアクセスする必要があります。 メソッドがファイナライザーから呼び出された場合 (disposing が false) は、他のオブジェクトにアクセスしないでください。 その理由は、オブジェクトが予測できない順序でファイナライズされるため、それら、またはその依存関係のいずれかが既にファイナライズされていることがあるためです。

また、このセクションは、破棄パターンをまだ実装していないベースを持つクラスにも適用されます。 パターンを既に実装しているクラスから継承する場合は、Dispose(bool) メソッドをオーバーライドして、追加のリソース クリーンアップ ロジックを提供するだけです。

アンマネージド リソースの解放に関連するすべてのロジックを一元化する protected virtual void Dispose(bool disposing) メソッドを宣言します。

このメソッドでは、すべてのリソースクリーンアップが行われる必要があります。 このメソッドは、ファイナライザーと IDisposable.Dispose メソッドの両方から呼び出されます。 ファイナライザー内から呼び出される場合、パラメーターは false になります。 これは、ファイナライズ中に実行されているコードが他のファイナライズ可能なオブジェクトにアクセスしないようにするために使用する必要があります。 ファイナライザーの実装の詳細については、次のセクションで説明します。

protected virtual void Dispose(bool disposing) {
    if (disposing) {
        if (resource!= null) resource.Dispose();
    }
}

Dispose(true)GC.SuppressFinalize(this) の順に呼び出すだけで、IDisposable インターフェイスを実装します。

SuppressFinalize の呼び出しは、Dispose(true) が正常に実行された場合にのみ発生する必要があります。

public void Dispose(){
    Dispose(true);
    GC.SuppressFinalize(this);
}

X パラメーターなしの Dispose メソッドを仮想にしないでください。

Dispose(bool) メソッドは、サブクラスによってオーバーライドされる必要があるメソッドです。

// bad design
public class DisposableResourceHolder : IDisposable {
    public virtual void Dispose() { ... }
    protected virtual void Dispose(bool disposing) { ... }
}

// good design
public class DisposableResourceHolder : IDisposable {
    public void Dispose() { ... }
    protected virtual void Dispose(bool disposing) { ... }
}

XDispose() および Dispose(bool) 以外の Dispose メソッドのオーバーロードを宣言しないでください。

Dispose は、このパターンを体系化し、実装者、ユーザー、コンパイラ間の混乱を防ぐために、予約語と見なす必要があります。 一部の言語では、特定の型にこのパターンを自動的に実装することを選択する場合があります。

Dispose(bool) メソッドを複数回呼び出せるようにします。 このメソッドは、最初の呼び出しの後に何もしないことを選択する場合があります。

public class DisposableResourceHolder : IDisposable {

    bool disposed = false;

    protected virtual void Dispose(bool disposing) {
        if (disposed) return;
        // cleanup
        ...
        disposed = true;
    }
}

X 包含プロセスが破損している重大な状況 (リーク、一貫性のない共有状態など) を除き、Dispose(bool) 内部から例外をスローすることを避けてください。

ユーザーは、Dispose の呼び出しで例外が発生しないことを期待しています。

Dispose で例外が発生する可能性がある場合、それ以上の finally-block クリーンアップ ロジックは実行されません。 これを回避するには、ユーザーが Dispose へのすべての呼び出しを (finally ブロック内で) try ブロックにラップする必要があります。これにより、非常に複雑なクリーンアップ ハンドラーが発生します。 Dispose(bool disposing) メソッドを実行する場合、破棄が false の場合は例外をスローしないでください。 これにより、ファイナライザー コンテキスト内で実行すると、プロセスが終了します。

オブジェクトが破棄された後に使用できない任意のメンバーから ObjectDisposedException をスローします。

public class DisposableResourceHolder : IDisposable {
    bool disposed = false;
    SafeHandle resource; // handle to a resource

    public void DoSomething() {
        if (disposed) throw new ObjectDisposedException(...);
        // now call some native methods using the resource
        ...
    }
    protected virtual void Dispose(bool disposing) {
        if (disposed) return;
        // cleanup
        ...
        disposed = true;
    }
}

close が領域内の標準的な用語である場合は、Dispose() に加えて Close() メソッドを提供することを検討してください。

これを行う場合は、Close の実装を Dispose と同じにし IDisposable.Dispose メソッドを明示的に実装することを検討することが重要です。

public class Stream : IDisposable {
    IDisposable.Dispose() {
        Close();
    }
    public void Close() {
        Dispose(true);
        GC.SuppressFinalize(this);
    }
}

ファイナライズ可能型

ファイナライズ可能型は、ファイナライザーをオーバーライドし、Dispose(bool) メソッドでファイナライズ コード パスを指定して、基本破棄パターンを拡張する型です。

ファイナライザーは正しく実装するのが難しいことで知られています。これは主に、システムの実行中にその状態に関する特定の (通常は有効な) 仮定を行うことができないことに起因します。 次のガイドラインを慎重に検討する必要があります。

ガイドラインの一部は、Finalize メソッドだけでなく、ファイナライザーから呼び出されるすべてのコードにも適用されることにご注意ください。 以前に定義した基本破棄パターンの場合、これは、disposing パラメーターが false の場合に Dispose(bool disposing) 内で実行されるロジックを意味します。

基底クラスが既にファイナライズ可能であり、基本破棄パターンを実装している場合は、Finalize をもう一度オーバーライドしないでください。 代わりに、Dispose(bool) メソッドをオーバーライドして、追加のリソース クリーンアップ ロジックを提供する必要があります。

次のコードは、ファイナライズ可能型の例を示しています。

public class ComplexResourceHolder : IDisposable {

    private IntPtr buffer; // unmanaged memory buffer
    private SafeHandle resource; // disposable handle to a resource

    public ComplexResourceHolder() {
        this.buffer = ... // allocates memory
        this.resource = ... // allocates the resource
    }

    protected virtual void Dispose(bool disposing) {
        ReleaseBuffer(buffer); // release unmanaged memory
        if (disposing) { // release other disposable objects
            if (resource!= null) resource.Dispose();
        }
    }

    ~ComplexResourceHolder() {
        Dispose(false);
    }

    public void Dispose() {
        Dispose(true);
        GC.SuppressFinalize(this);
    }
}

X 型をファイナライズ可能にすることは避けてください。

ファイナライザーが必要と思われる場合は慎重に検討してください。 パフォーマンスとコードの複雑さの両方の観点から、ファイナライザーを持つインスタンスに関連する実際のコストがあります。 SafeHandle などのリソース ラッパーを使用して、可能な限りアンマネージド リソースをカプセル化することを優先します。その場合、ラッパーが独自のリソース クリーンアップを担うため、ファイナライザーは不要になります。

X 値の型をファイナライズ可能にしないでください。

CLR によって実際に最終処理されるのは参照型のみであるため、値の型にファイナライザーを配置しようとすると無視されます。 C# および C++ コンパイラにより、この規則が適用されます。

型が、独自のファイナライザーを持たないアンマネージド リソースの解放を担う場合に、型をファイナライズ可能にします。

ファイナライザーを実装するときは、Dispose(false) を呼び出し、すべてのリソース クリーンアップ ロジックを Dispose(bool disposing) メソッド内に配置するだけです。

public class ComplexResourceHolder : IDisposable {

    ~ComplexResourceHolder() {
        Dispose(false);
    }

    protected virtual void Dispose(bool disposing) {
        ...
    }
}

すべてのファイナライズ可能型に基本破棄パターンを実装します。

これにより、その型のユーザーに、ファイナライザーが担うのと同じリソースの決定論的クリーンアップを明示的に実行する手段が提供されます。

X ファイナライザー コード パス内のファイナライズ可能なオブジェクトにはアクセスしないでください。それらが既にファイナライズ済みであるという重大なリスクがあるためです。

たとえば、ファイナライズ可能オブジェクト B への参照を持つファイナライズ可能オブジェクト A は、A のファイナライザー内で B を確実に使用することができません。また、その逆も同様です。 ファイナライザーは無作為な順序で呼び出されます (クリティカル ファイナライズの弱い順序付け保証はありません)。

また、静的変数に格納されているオブジェクトは、アプリケーション ドメインのアンロード中またはプロセスの終了時の特定の時点で収集されることにご注意ください。 ファイナライズ可能オブジェクトを参照する静的変数にアクセスする (または静的変数に格納されている値を使用する可能性がある静的メソッドを呼び出す) 場合、Environment.HasShutdownStarted が true を返す場合は、安全ではないことがあります。

Finalize メソッドを保護します。

C#、C++、VB.NET 開発者は、このガイドラインを適用するのにコンパイラが役立つので、このことを心配する必要はありません。

X システムクリティカルなエラーを除き、ファイナライザー ロジックから例外をエスケープしないでください。

ファイナライザーから例外がスローされた場合、CLR がプロセス全体 (.NET Framework バージョン 2.0 時点で) をシャットダウンし、他のファイナライザーが実行されることと、リソースが制御された方法で解放されることを防ぎます。

強制されたアプリケーション ドメインのアンロードおよびスレッドの中止に直面している場合でも、ファイナライザーを必ず実行する必要がある状況では、クリティカル ファイナライズ可能オブジェクト (CriticalFinalizerObject を含む型階層がある型) の使用を検討してください。

Portions © 2005, 2009 Microsoft Corporation. All rights reserved.

2008 年 10 月 22 日に Microsoft Windows Development シリーズの一部として、Addison-Wesley Professional によって発行された、Krzysztof Cwalina および Brad Abrams による「Framework Design Guidelines: Conventions, Idioms, and Patterns for Reusable .NET Libraries, 2nd Edition」 (フレームワーク デザイン ガイドライン: 再利用可能な .NET ライブラリの規則、用法、パターン、第 2 版) から Pearson Education, Inc. の許可を得て再印刷されています。

関連項目