前の記事で整理したアンマネージドリソースの問題に対して、.NETではその解決手段として IDisposable という仕組みが用意されています。ここでは、その役割や設計の考え方、基本的な使い方について整理します。
IDisposableとは何か
IDisposable は、アンマネージドリソース(およびそれに準ずるリソース)を 明示的に解放するための仕組み です。
public interface IDisposable
{
void Dispose();
}
インターフェース自体は非常にシンプルですが、この Dispose() には明確な意味があります。
「このオブジェクトはもう使い終わったので、保持しているリソースを解放する」 という合図です。
C#では通常、メモリの解放はGCが自動で行ってくれます。しかし、それとは別に「自分で終わりを明示しなければならないもの」が存在し、そのための共通ルールとして用意されているのが IDisposable です。
なぜ必要なのか
前提として、GCが扱っているのは「マネージドメモリ」だけです。 これは前の記事で整理した通りです。
GCの動作は非常にシンプルで、
- オブジェクトへの参照があるかどうか
- 参照がなければ回収する
というルールで動いています。
ここで重要なのは、「リソースが使われているかどうか」は見ていない という点です。
例えば、次のコードを考えます。
var stream = new FileStream("test.txt", FileMode.Open);
このとき実際には、
- OS側にファイルハンドルが作られる
- C#オブジェクトがそれを保持する
という構造になっています。
問題の本質
- GCが見ているのは「FileStreamオブジェクト」だけ
- OSのファイル状態は認識していない
そのため、
- ファイルが開いたままになる
- 解放タイミングが不定になる
といった問題が発生します。
この「GCでは制御できない領域」を扱うために IDisposable が必要になります。
基本的な使い方(using)
IDisposableの最も基本的な使い方は using 文です。
using (var stream = new FileStream("test.txt", FileMode.Open))
{
// 処理
}
この構文は単なる便利記法ではなく、内部的には次のような構造になります。
var stream = new FileStream(...);
try
{
// 処理
}
finally
{
stream.Dispose();
}
ポイント
- 例外が発生しても必ずDisposeされる
- スコープ終了時に解放される
つまり、「確実にリソースを解放するための仕組み」 として機能します。
どんなものが対象か
IDisposableが関係するのは、主に「外部リソースを扱うもの」です。
典型例
FileStream(ファイル)StreamReaderSocket(通信)SqlConnection(DB接続)
これらはすべて、「開く/使う/閉じる」というライフサイクルを持つ という共通点があります。
また、直接アンマネージドリソースを扱っていなくても、
間接的なケース
- 内部でアンマネージドリソースを持つクラス
- 内部に IDisposable を持つクラス
も対象になります。
ここで重要なのは、「.NETのクラスだから安全」というわけではない という点です。
重要な理解:オブジェクトとリソースは別
IDisposableを理解する上で重要なのは、この構造です。
var stream = new FileStream(...);
実際の構造
| 層 | 内容 |
|---|---|
| C#オブジェクト | マネージド |
| 内部のリソース | アンマネージド |
つまり、マネージドオブジェクトがリソースを“保持しているだけ” です。
この構造のため、
- オブジェクトの寿命
- リソースの寿命
は必ずしも一致しません。
このズレを埋めるのが Dispose の役割です
設計の本質
IDisposableの設計で最も重要なのは、「誰が解放責任を持つか」です。
基本ルール
- 自分で生成したリソース → 自分でDisposeする
- 外から渡されたリソース → 勝手にDisposeしない
例:所有している場合
class A : IDisposable
{
private FileStream _stream;
public A()
{
_stream = new FileStream(...);
}
public void Dispose()
{
_stream.Dispose();
}
}
この場合、A がリソースの所有者なので、解放責任も A にあります。
例:借りている場合
class A
{
private Stream _stream;
public A(Stream stream)
{
_stream = stream;
}
}
この場合、Stream の所有者は呼び出し側なので、AはDisposeしない(責任を持たない) が正解です。
IDisposableは連鎖する
もう一つ重要な性質として、「Disposeは上位に伝播する」というものがあります。
構造
A → B → C → FileStream
- FileStreamがリソースを持つ
- Cがそれを保持
- BがCを保持
- AがBを保持
最終的にAまで責任が伝わります。
実務的なルール
IDisposableを持つフィールドがある場合、自分もIDisposableになる可能性が高い
自分で実装する場合
シンプルなパターン
public class Sample : IDisposable
{
private bool _disposed;
public void Dispose()
{
if (_disposed) return;
// リソース解放
_disposed = true;
}
}
本格パターン
public class Sample : IDisposable
{
private bool _disposed;
protected virtual void Dispose(bool disposing)
{
if (_disposed) return;
if (disposing)
{
// マネージドリソース
}
// アンマネージドリソース
_disposed = true;
}
public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}
~Sample()
{
Dispose(false);
}
}
これは「アンマネージドリソースを直接扱う場合」に必要になるパターンです。
よくある落とし穴
- Disposeを忘れる → リソースが解放されない
- Dispose後に使う →
ObjectDisposedException - スレッドをまたいでDispose → 状態不整合の原因
まとめ
IDisposableの本質
- 明示的にリソースを解放するための仕組み
- GCでは扱えない領域を補う
設計の核心
- 「所有者が解放責任を持つ」
実務ルール
- IDisposableはusingで扱う
- 自分で生成したものは自分でDispose
- 渡されたものは勝手にDisposeしない
最重要ポイント
- オブジェクトの寿命とリソースの寿命は別
この理解があると、「なぜDisposeが必要なのか」「なぜusingを書くのか」が自然に繋がるようになります。