CLR 徹底解剖
IDisposable について
Shawn Farkas

コンテンツ
共通言語ランタイム (CLR) がマネージ コードの開発者に提供している生産性向上のための機能の 1 つに、マネージ ヒープに割り当てられたメモリが不要になるとガベージ コレクタ (GC) によってクリーンアップされるという機能があります。 これにより、開発者は、メモリのリークや、解放されたメモリを使おうとしたり、メモリを二重解放したりすることによって生じる困難な問題のデバッグに膨大な時間を費やす必要がなくなりました。 しかし、ガベージ コレクタはメモリのリークを防ぐのには有効ですが、解放する必要がある他のリソースについての情報は保持していません。 たとえば、ガベージ コレクタは、ファイル ハンドルを閉じる方法や、CoAllocTaskMem などの API を使用してマネージ ヒープの外部に割り当てられたメモリを解放する方法を知りません。
そのような種類のリソースを管理するオブジェクトは、リソースが不要になったときに確実に解放されるようにする必要があります。 そのためには、System.Object の Finalize メソッドをオーバーライドします。これにより、オブジェクトが自身のクリーンアップを実行しようとしていることをガベージ コレクタに通知できます (C# では、このメソッドを直接オーバーライドするのではなく、C++ のデストラクタ構文である ~MyObject を使用します)。 クラスにファイナライザがある場合、ガベージ コレクタは、その型のオブジェクトを収集する前にオブジェクトのファイナライザを呼び出して、オブジェクトが保持しているすべてのリソースのクリーンアップを実行できるようにします。
このシステムにおける問題の 1 つは、ガベージ コレクタが確定的に実行されないことであり、その結果として、オブジェクトへの最後の参照がなくなった後も長時間にわたってオブジェクトのファイナライズが実行されない可能性があることです。オブジェクトが高価または希少なリソース (データベース接続など) を保持する場合、これは許容できない問題になります。たとえば、利用可能な開いている接続が 10 個しかなく、そのうちの 1 つをオブジェクトが保持している場合、オブジェクトは、ガベージ コレクタがファイナライズ メソッドを呼び出すのを待たずにできるだけ早くその接続を解放する必要があります。
破棄可能なオブジェクト
不確定な期間にわたってガベージ コレクタの実行を待機する必要をなくすには、リソースを所有する型が IDisposable インターフェイスを実装する必要があります。これにより、その型のコンシューマは適切なタイミングでリソースを解放できるようになります。 IDisposable の実装は、オブジェクトの操作が終了した直後に Dispose を呼び出す必要があることをオブジェクトのコンシューマに示し、不要になったリソースをオブジェクトが直ちに解放できるようにします。 C# と Visual Basic® には、このプロセスをできるだけ簡単にするための "using" キーワードが用意されています。 C# では、IDisposable を実装している MyClass クラスがある場合、次のコードによってコンパイラに図 1 のようなコードを生成させることができます。

Figure 1 生成されるコード
MyClass myClass = GetMyClass();
try
{
myClass.DoSomething();
}
finally
{
IDisposable disposable = myClass as IDisposable;
if (disposable != null) disposable.Dispose();
}
using (MyClass myClass = GetMyClass())
{
myClass.DoSomething();
}
try/finally ブロックを使用すると、using ブロック内のコードで例外が発生した場合でも、MyClass の Dispose メソッドはオブジェクトのクリーンアップを確実に実行できます。 コンパイラによって生成されたコードは、Dispose メソッドを呼び出す前に 2 つの興味深い処理を行います。 まず、破棄可能なオブジェクトが null でないことを確認します。そのため、変数に割り当てられた式が null と評価されても、アプリケーションの using ブロックでは NullReferenceException が発生しません。 また、このコードは IDisposable への参照を経由して Dispose を呼び出すことにより、IDisposable を明示的に実装している型が正しくクリーンアップされるようにします。
using ブロックは IDisposable の明示的な実装を持つクラスと連動しますが、このような方法でクラスに IDisposable インターフェイスを実装することはお勧めできません。 IDisposable を明示的に実装した場合、Visual Studio® の IntelliSense® を使用してオブジェクト モデルを探索している開発者は、オブジェクトに Dispose メソッドがあることに気付かず、したがって早期のクリーンアップを利用しません。その結果、オブジェクトはガベージ コレクタがファイナライザを実行するまでリソースを保持することになります。
ガベージ コレクタの観点から見ると IDisposable は 1 つのインターフェイスにすぎないという点に注意してください。 ガベージ コレクタは、破棄可能なオブジェクトを IDisposable の実装を持たないオブジェクトと同じように扱います。具体的には、ガベージ コレクタは Dispose メソッドを自動的に呼び出しません。 ガベージ コレクタが呼び出すクリーンアップ メソッドは、オブジェクトのファイナライザだけです。Dispose メソッドを明示的に呼び出すコードを記述していなければ、Dispose メソッドは呼び出されません。
Disposable パターン
既に説明したように、C# と Visual Basic の "using" コンストラクトを使用すると、破棄可能なオブジェクトを簡単にクリーンアップできます。 ただし、そのためには開発者が破棄可能な型を作成する必要があります。 IDisposable インターフェイスは 1 つのメソッドしか定義していないため、破棄可能な型の開発者はこのインターフェイスをすばやく簡単に実装できるように思われます。 しかし、Joe Duffy の
IDisposable guidelines で述べられているように、本当に強固な破棄可能な型を実装するにはさまざまな細かい配慮が必要です。
Dispose メソッドの主な特徴の 1 つに、このメソッドが行うクリーンアップが、オブジェクトのファイナライザによるクリーンアップとよく似ているという点があります。 コードの重複を避けることは常に望ましいため、2 つの場所の間でこのクリーンアップ コードをコピーして貼り付けるよりも、共有する方が適切です。 この方法には、型のクリーンアップ コードを 1 か所にまとめることができるという利点もあります。 このコードを共有する明白な方法は、すべてのクリーンアップ コードを Dispose メソッドに含めて、オブジェクトのファイナライザが単純に Dispose を呼び出すようにすることです。
public class MyClass : IDisposable
{
~MyClass()
{
Dispose();
}
public void Dispose()
{
// Cleanup
}
}
この場合、すべてのクリーンアップ コードを 1 か所にまとめるという目標は確かに達成できますが、ファイナライザの実装に問題が生じます。 一部のクリーンアップ コードは Dispose とファイナライザの間で共有できますが、Dispose メソッドでは解放したいがファイナライズでは解放したくない一連のリソースも存在するからです。
マネージ リソースとネイティブ リソース
ファイナライザは確定的な順序で実行されないため、オブジェクトが参照する他のオブジェクトが、ファイナライザが実行される時点でまだファイナライズされていないことは保証できません。 たとえば、次のクラスについて考えてみましょう。
public class Database : IDisposable
{
private NetworkConnection connection;
private IntPtr fileHandle;
// ...
}
public class NetworkConnection : IDisposable
{
private IntPtr connectionHandle;
// ...
}
Database クラスには、NetworkConnection 型と、ファイル ハンドルを表す IntPtr 型の 2 つのリソースが含まれています。 Database 型のオブジェクトへのアクティブな参照が残っている間は、ガベージ コレクタは connection メンバ変数によって参照される NetworkConnection オブジェクトを収集しません。 しかし、ガベージ コレクタが Database オブジェクトのファイナライザを呼び出している場合は、このオブジェクトへのライブ参照が残っておらず、オブジェクトが収集できる状態になったことがわかります。
NetworkConnection オブジェクトへのライブ参照が残っていない場合、データベースのファイナライザを実行する前に NetworkConnection オブジェクトがファイナライズされたという可能性もあります。 既にファイナライズされているオブジェクトを使用することは、オブジェクトが使用可能な状態になる可能性が低いためお勧めできません。ファイナライズの順序に関しては保証がないので、Database のファイナライザが connection フィールドを参照しないようにする必要があります。
しかし、Dispose メソッドの呼び出しの結果としてクリーンアップを実行している場合は、Dispose のオブジェクトへのライブ参照がないと Dispose を呼び出せないことから、ファイナライズが行われていないことがわかります。 Database オブジェクトはまだ終了しておらず、connection フィールドによって参照される NetworkConnection の GC ルートとして機能しているため、NetworkConnection オブジェクトはファイナライズされません。 したがって、Dispose メソッドでは、ファイナライズ可能なメンバ変数に安全にアクセスできます。
ただし、fileHandle フィールドについては事情が異なります。 このハンドル フィールドは単なる IntPtr で、ガベージ コレクタは IntPtr をファイナライズしません。 つまり、ファイナライザと Dispose メソッドの両方で、fileHandle フィールドに安全にアクセスできます。
この原則を一般化すると、Dispose メソッドでは、オブジェクトが保持しているすべてのリソースを、マネージ オブジェクトかネイティブ リソースかに関わらず、安全にクリーンアップできるということになります。 しかし、ファイナライザではファイナライズ可能でないオブジェクトだけを安全にクリーンアップでき、一般的にはファイナライザがネイティブ リソースだけを解放するようにする必要があります。
この例で興味深いのは、connection フィールドが明らかにネイティブな接続ハンドルを含んでいるにもかかわらず、ネイティブ リソースと見なされないという点です。 NetworkConnection クラスの内部実装は確かにネイティブ リソースを含んでいますが、NetworkConnection 型はそれ自身のリソースの有効期間を管理するため、Database クラスからはマネージ リソースと見なされます。 しかし、NetworkConnection クラス内では、connectionHandle はコードで明示的に記述されていない限りクリーンアップされないため、ネイティブ リソースと見なされます。
破棄ではすべてのリソースをクリーンアップする必要があり、ファイナライズではこれらのリソースのサブセットをクリーンアップする必要があることから、ファイナライザと Dispose メソッドの両方から呼び出されるメソッドにクリーンアップ コードを含めることができます。 この新しいメソッドでは、破棄を実行する場合はすべてのリソースがクリーンアップされ、オブジェクトをファイナライズする場合はネイティブ リソースだけがクリーンアップされるようにします。 そのためには、図 2 のパターンに従った Disposable の実装を作成する必要があります。

Figure 2 基本的な IDisposable パターン
public class DisposableClass : IDisposable
{
~DisposableClass()
{
Dispose(false);
}
public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}
protected virtual void Dispose(bool disposing)
{
if (disposing)
{
// Clean up all managed resources
}
// Clean up all native resources
}
}
この場合も、すべてのクリーンアップ コードが Dispose メソッドのオーバーライドに含まれているため、コードの重複を避けてクリーンアップ コードを 1 つの場所にまとめるという当初の目標は達成されています。 他の Dispose メソッドとの混同を避けるため、この記事の残りの部分ではこのメソッドを "クリーンアップ メソッド" と呼ぶことにします。 クリーンアップ メソッドは、現在のパスが破棄コード パスとファイナライズ コード パスのどちらであるかを示すブール型パラメータを受け取ります。 両方のコード パスで、クラスが保持しているネイティブ リソースをクリーンアップする必要があります。 さらに、Dispose コード パスでは、マネージ リソースもクリーンアップする必要があります。
クラスのコンシューマがクリーンアップ メソッドを直接呼び出せるようにはしないので、クリーンアップ メソッドは public である必要はありません。 その代わりに、コンシューマは IDisposable インターフェイス経由でクリーンアップ ロジックにアクセスできます。 クリーンアップ メソッドは、クラスのコンシューマから直接呼び出す必要はありませんが、サブクラスからは呼び出す必要があるため、private ではなく protected としてマークされています (破棄可能な型のサブクラス化については後で説明します)。
Dispose はオブジェクトが保持しているすべてのリソースを解放するため、ガベージ コレクタがオブジェクトをファイナライズする必要はありません。 ファイナライザがある場合でもオブジェクトのファイナライズを実行する必要がないことをガベージ コレクタに知らせるには、Dispose メソッドでクリーンアップ コードを実行した後に GC.SuppressFinalize を呼び出します。
実際には、型がネイティブ リソースを所有していないと、ファイナライザによって実行されるコードは何の操作も行いません。 そのような場合には、クラスでファイナライザ メソッドを定義しないでください。
Database の例では、新しいクリーンアップ メソッドは図 3 のようになります。 Database オブジェクトの破棄では、connection マネージ リソースと fileHandle ネイティブ リソースの両方がクリーンアップされます。 しかし、オブジェクトのファイナライズでは、fileHandle リソースだけがクリーンアップされます。 このクリーンアップ コードは、複数回にわたって安全に呼び出すことができます。 Dispose メソッドの最初の呼び出しでは、Database オブジェクトが保持しているすべてのリソースが解放されます。それ以降の Dispose の呼び出しでは、リソースが既に解放されていることが検出され、解放を試みずに呼び出しが終了します。 追加の呼び出しは不要ですが、複数回呼び出されても成功するように Dispose の実装をコーディングしてください。 オブジェクトの他のメソッドは、オブジェクトが破棄された後に呼び出されると ObjectDisposedException をスローすることがあります。

Figure 3 Database のクリーンアップ メソッドの例
protected virtual void Dispose(bool disposing)
{
if (disposing)
{
if (connection != null)
connection.Dispose();
}
if (fileHandle != IntPtr.Zero)
{
CloseHandle(fileHandle);
fileHandle = IntPtr.Zero;
}
}
また、興味深いのは、クリーンアップ コードが例外をスローしないことです。 通常、開発者は、例外を発生させずにオブジェクトをクリーンアップできるようにしたいと考えます。 これは、クリーンアップ コード パスの途中で例外から回復する適切な方法がないからです。 クリーンアップ中にオブジェクトが失敗した場合、オブジェクトのコンシューマは、再試行して 2 回目のクリーンアップが成功するよう祈るか、一部のリソースが正しく解放されなくてもあきらめるしかありません。
クリーンアップ コードが例外をスローしないようにするには、クリーンアップ メソッドでアクセスするすべてのオブジェクト参照が null でないことを確認して、失敗しないメソッドだけを呼び出すようにする必要があります。 このことは、オブジェクトがファイナライズされるときに非常に重要になります。 Dispose の呼び出し中に例外が発生しても開発者の立場が悪くなるだけですが、ファイナライザ スレッドで処理不能な例外が発生すると、既定では CLR のバージョン 2.0 のプロセス全体が破壊されます。
ファイナライザ スレッドを実行していなくても、クリーンアップ中に例外がスローされると、他のリソースが期待どおりにクリーンアップされないおそれがあります。 たとえば、図 4 を見てください。 ここで、DisposableClass には Foo 型と Bar 型のリソースが含まれています。 Foo.Dispose が実行中に例外をスローした場合、この DisposableClass は Bar オブジェクトを破棄できなくなります。

Figure 4 Dispose における例外の危険性
public class Disposableclass : IDisposable
{
Foo a;
Bar b;
// ...
protected virtual void Dispose(bool disposing)
{
if (disposing)
{
if (a != null) a.Dispose();
if (b != null) b.Dispose();
}
}
}
マネージ リソースのクリーンアップ
オブジェクトをファイナライズするときにマネージ リソースがクリーンアップされないならば、どのようにしてマネージ リソースのリークを防止できるのでしょうか。 クリーンアップの必要があるリソースを含んでいる場合、オブジェクトは IDisposable パターンに従う必要があります。 オブジェクトのファイナライザはマネージ リソースが既にファイナライズされているかどうかを確認できないため、マネージ リソースを直接クリーンアップできません。 しかし、これらの型が解放する必要のあるリソースを含んでいる場合は、ガベージ コレクタが実行できるファイナライザも持っている必要があります。マネージ リソースだけを含んでいる場合は、ファイナライザは必要ありません。
例を見てみましょう。 マネージ コントロールがブラウザ セッション中に状態を維持するために使用する Cookie クラスがあるとします。 Cookie クラスは IsolatedStorage を使用して実装でき、IsolatedStorage 自体は FileStream を使用して実装できます。 FileStream は Win32® ファイル ハンドルを含むため、最終的なクラスは図 5 に示すコードのようになります。

Figure 5 クラス階層での IDisposable の実装
public class FileStream : IDisposable
{
private IntPtr fileHandle;
// ...
protected void Dispose(bool disposing)
{
if (fileHandle != IntPtr.Zero)
{
CloseHandle(fileHandle);
fileHandle = IntPtr.Zero;
}
}
}
public class IsolatedStorageFileStream : IDisposable
{
private FileStream file;
// ...
protected void Dispose(bool disposing)
{
if (disposing)
{
if (file != null)
file.Dispose();
}
}
}
public class Cookie : IDisposable
{
private IsolatedStorageFileStream isolatedStore;
// ...
protected void Dispose(bool disposing)
{
if (disposing)
{
if (isolatedStore != null)
isolatedStore.Dispose();
}
}
}
コントロールが Cookie オブジェクトを破棄し忘れた場合、Cookie は IsolatedStorageFileStream をクリーンアップするためのファイナライザを持ちません。これは、IsolatedStorageFileStream がマネージ リソースであるからです。 ガベージ コレクタはストリーム オブジェクトを自動的に回収するので、それで問題ありません。 さらに、IsolatedStorageFileStream はマネージ リソースだけを含んでいるため、ファイナライザを必要としません。 FileStream オブジェクトはネイティブ リソースを所有しており、ファイナライザを含むため、これも問題ありません。 ガベージ コレクタは、FileStream をファイナライズするときにハンドルを閉じます。 誰もこのオブジェクト グラフのルート オブジェクトを破棄しなくても、関係するすべてのクラスが Disposable パターンに従っているため、リソースのリークは発生しません。
Disposable 型からの派生
破棄可能な型からの派生を行う場合に、派生型が新しいリソースを含んでいなければ、特別な処理は不要です。 IDisposable 基本型の実装がリソースのクリーンアップを行うため、サブクラスが詳細を意識する必要はまったくありません。 ただし、サブクラスにはクリーンアップを必要とする新しいリソースが含まれているのが一般的です。 その場合、クラスはそれ自身のリソースを解放すると同時に、基本型のリソースが確実に開放されるようにする必要があります。 そのためには、クリーンアップ メソッドをオーバーライドし、リソースを解放した後で、図 6 に示すように基本型を呼び出してそのリソースをクリーンアップします。

Figure 6 Dispose のオーバーライド
public class DisposableBase : IDisposable
{
~DisposableBase()
{
Dispose(false);
}
public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}
protected virtual void Dispose(bool disposing)
{
// ...
}
}
public class DisposableSubclass : DisposableBase
{
protected override void Dispose(bool disposing)
{
try
{
if (disposing)
{
// Clean up managed resources
}
// Clean up native resources
}
finally
{
base.Dispose(disposing);
}
}
}
継承したバージョンの public な Dispose メソッドとファイナライザは目的どおりに動作するので、派生クラスがクリーンアップ メソッドだけをオーバーライドする必要がある点に注意してください。 この原則の例外は、基本型がネイティブ リソースを所有せず、派生型がたまたまネイティブ リソースを所有しているために、基本型がファイナライザを持たない場合です。 その場合は、派生型にファイナライザを追加する必要があります。
派生型では、基本型のクリーンアップ メソッドを呼び出す前にリソースをクリーンアップしてください。 これによって、オブジェクトが構築されたのと逆の順序で分解されます。 基本型のクリーンアップ メソッドを最初に呼び出した場合は、クリーンアップ メソッドを完了するために必要なオブジェクトの一部が、基本型によって破壊された状態になる恐れがあります。
Dispose とセキュリティ
クリーンアップ コードの記述における微妙な問題の 1 つに、コードを実行できるセキュリティ コンテキストがあります。クリーンアップ コードはすべて、実行中のスレッドがどの ID を偽装していても実行できるようにする必要があります。 ガベージ コレクタは専用スレッドでファイナライザを実行するので、破棄可能なオブジェクトをインスタンス化したスレッドで有効になっている偽装があっても、ファイナライザが実行されるときには有効になりません。 また、型を使用するコードがオブジェクトの構築と Dispose 呼び出しの間にスレッドの偽装を元に戻したり、別の ID を共に偽装したりしていないことを確認する方法はありません。
偽装を正しく処理しないクラスの 1 つの例として、Microsoft® .NET Framework 2.0 の RSACryptoServiceProvider クラスがあります。RSACryptoServiceProvider オブジェクトが使用する短期キーを作成する場合、オブジェクトのファイナライザはそのキーを削除しようとします。このキーは、キーを作成するスレッドを実行しているユーザーのプロファイルに保存されます。 スレッドがユーザーを偽装している間に RSACryptoServiceProvider オブジェクトが作成されると問題が発生し、オブジェクトが破棄されません。 その場合、ファイナライザはオブジェクトのコンストラクタとは別のユーザーのコンテキストで実行されます。 ファイナライザがキーを削除しようとすると、ファイナライザ スレッドを実行しているユーザーがキーの保存されているユーザー プロファイルにアクセスできないため、例外が発生します。
ファイナライザが別スレッドで実行されるということは、Dispose メソッドのコール スタックのアクセス許可チェックを信頼できないということも意味します。 クリーンアップ メソッドがオブジェクトのファイナライザから呼び出されている場合、コール スタックの上位にコードがないため、Code Access Security のチェックが無意味になります (一般に、セキュリティ要求は例外を発生させる可能性があるので、クリーンアップ コードでは避ける必要があります)。
SafeHandles
.NET Framework 2.0 では、リソース管理を支援するために SafeHandle クラスが導入されています。 SafeHandle はネイティブ リソースをラップし、IntPtr を使用してリソースを保持する場合に比べていくつかの利点があります。 SafeHandle の完全な詳細についてはこの記事では説明しませんが、詳細については、基本クラス ライブラリ (BCL) に関する Brian Grunkemeyer の秀逸な記事
team blog を参照してください。
重要なのは、SafeHandle を導入すると、SafeHandle から派生していない型が生のネイティブ リソースを含む必要がなくなるという点です。 その代わりに、オブジェクトにはマネージ リソース (使用するネイティブ リソースの SafeHandle のサブクラスなど) だけを含める必要があります。
このパターンに従うことにはいくつかの利点があります。 第一に、SafeHandles がネイティブ リソースを所有するため、SafeHandles 以外のクラスがファイナライザを持つ必要はありません。 第二に、各 SafeHandle がネイティブ リソースを 1 つだけ所有する簡潔なモデルを作成でき、マネージ オブジェクトが必要に応じてさまざまな SafeHandle を構成できます。
まとめ
CLR のガベージ コレクタのおかげで、管理者はマネージ ヒープに割り当てられたメモリの管理に煩わされずに済むようになりました。しかし、その他の種類のリソースに対しては依然としてクリーンアップを実行する必要があります。 ガベージ コレクタがオブジェクトのファイナライズを実行する前にコンシューマが高価なリソースを解放できるようにするには、マネージ クラスに IDisposable インターフェイスを使用します。 Disposable パターンに従い、注意する必要がある問題を認識することにより、クラスのすべてのリソースを正しくクリーンアップでき、Dispose を呼び出してクリーンアップ コードを直接実行する場合とファイナライザ スレッドを使用してクリーンアップ コードを実行する場合の両方で問題の発生を防ぐことができます。
ご意見やご質問は、
こちら clrinout@microsoft.com.
まで英語でお送りください。
Shawn Farkasは、Microsoft の CLR セキュリティ チームのソフトウェア設計エンジニアで、セキュリティ ポリシー、暗号化、および ClickOnce に携わっています。 彼のブログは、
blogs.msdn.com/shawnfa で公開されています。