前の記事では Finalize() を整理し、ファイナライザは 「Dispose の呼び忘れに対する保険」 に過ぎないことを確認しました。では本来の解放手段である Dispose() とは何か、なぜそれが望ましいのか——この記事では IDisposable インターフェースと Dispose() メソッド、そして using 構文の使い方を整理します。
IDisposable の基本については既存の記事でも触れていますが、ここでは Finalize の代替 という観点から、より深く掘り下げます。
なぜ Finalize() の代替が必要か
Finalize() には次のような根本的な弱点があります。
- タイミングが不定:いつ呼ばれるか分からない
- GC を重くする:ファイナライズキュー、世代昇格
- 保証がない:プロセス強制終了時には呼ばれない
- スレッドが 1 本:詰まると全体に波及
つまり、「リソースを必要なくなった瞬間に解放したい」という当然の要求に Finalize() だけでは答えられません。
そこで登場するのが、呼び出し側が「もう要らない」と明示的に宣言する仕組み ——IDisposable です。
IDisposable インターフェース
IDisposable の定義は驚くほどシンプルです。
namespace System
{
public interface IDisposable
{
void Dispose();
}
}
たった 1 つのメソッドだけ。しかしこの Dispose() には、
「このオブジェクトはもう使い終わった。保持しているリソースを今すぐ解放してよい」
という強い意味が込められています。
Finalize() との違い
| 観点 | Finalize() |
Dispose() |
|---|---|---|
| 呼び出す主体 | GC | 呼び出し側コード |
| 呼び出しタイミング | 不定 | 呼んだ瞬間 |
| 確実性 | 保証なし | 呼べば必ず実行 |
| 対象 | アンマネージドのみ推奨 | マネージド・アンマネージド両方 OK |
| GC への負担 | 大 | なし |
Dispose() は 決定論的(deterministic)な解放 を提供します。これが Finalize() との最大の違いです。
using 文 — Dispose を確実に呼ぶ
Dispose() を呼び忘れたら本末転倒です。そこで C# は using 文という構文糖を用意しています。
using (var stream = new FileStream("test.txt", FileMode.Open))
{
// stream を使う処理
}
// ここでスコープを抜けると同時に stream.Dispose() が呼ばれる
これは内部的に try/finally に展開されます。
var stream = new FileStream("test.txt", FileMode.Open);
try
{
// stream を使う処理
}
finally
{
stream?.Dispose();
}
ポイントは、
- 例外が発生しても必ず Dispose される
- スコープを抜けたタイミングで解放される
- 書き忘れにくい
ということ。「リソースを開いたら、その場で using で囲む」が C# の基本作法です。
複数リソースの using
using は入れ子にできますし、変数を並べることもできます。
// 入れ子(暗黙のブロック省略形)
using (var conn = new SqlConnection(connStr))
using (var cmd = conn.CreateCommand())
{
// ...
}
// 同じ型なら並列宣言も可能
using (FileStream a = File.OpenRead("a.txt"),
b = File.OpenRead("b.txt"))
{
// ...
}
解放順は 後に宣言したものから先に Dispose(LIFO)されます。
using 宣言(C# 8.0)
C# 8.0 では、より簡潔な書き方として using 宣言(using declaration) が導入されました。波括弧でブロックを作らず、変数宣言の前に using を付けるだけです。
public void ReadFile(string path)
{
using var stream = new FileStream(path, FileMode.Open);
using var reader = new StreamReader(stream);
string content = reader.ReadToEnd();
Console.WriteLine(content);
// メソッドを抜けるときに reader → stream の順に Dispose される
}
何が変わったか
- インデントが深くならない:従来の
using (...) { ... }のブロック地獄から解放される - スコープは「変数を宣言したスコープの終わり」まで:通常はメソッド末尾、
if/forの中なら そのブロックの終わり - 解放順は逆順(LIFO)
従来の using 文との使い分け
| 状況 | 推奨 |
|---|---|
| リソースをメソッド全体で使う | using 宣言 |
| 一部のスコープでだけ使いたい | using 文(ブロック) |
| すぐに Dispose したい区切りがある | using 文(ブロック) |
例えば「ファイルを読み終わったらすぐ閉じて、その後に重い処理をしたい」場合は、明示的なブロックの方が意図が伝わります。逆に、メソッド全体でリソースを使うだけなら、using 宣言の方が読みやすいです。
public string ReadAndProcess(string path)
{
string content;
using (var reader = new StreamReader(path))
{
content = reader.ReadToEnd();
} // ここで明示的にファイルを閉じる
// ファイルを閉じてから重い処理
return ExpensiveTransform(content);
}
IAsyncDisposable(C# 8.0+)
非同期で解放処理を行いたいケースのために、IAsyncDisposable が追加されました。
public interface IAsyncDisposable
{
ValueTask DisposeAsync();
}
非同期で I/O フラッシュやネットワーク切断を行いたいリソース(Stream、DbConnection の一部、gRPC チャネルなど)が実装しています。
await using 構文で扱います。
public async Task ProcessAsync()
{
await using var conn = new SqlConnection(connStr);
await conn.OpenAsync();
// ...
} // メソッド終了時に await conn.DisposeAsync() が呼ばれる
IAsyncDisposable と IDisposable の両方を実装する型もあります(FileStream など)。非同期コンテキストでは await using を優先 すると覚えておけば十分です。
Dispose の使いどころ
Dispose を実装すべき・呼ぶべきケースをまとめます。
実装すべきクラス
- アンマネージドリソース(OS ハンドル、ネイティブメモリなど)を直接持つ
- 内部に
IDisposableフィールドを持つ - イベントハンドラを購読しており、解除する必要がある
- 接続・セッション・ロックなど、明示的な「終了」が必要
呼び出し側のルール
- 自分で生成した
IDisposableは自分で破棄する:usingで囲むのが基本 - 引数で渡された
IDisposableは勝手に破棄しない:呼び出し側が所有者 - フィールドに保持した
IDisposableは、保持しているクラスもIDisposableにする
このあたりの所有権の考え方は既存記事でも整理しています。
Finalize と Dispose の関係
両者は対立するものではなく、役割分担 をします。
- Dispose:通常の解放経路。決定論的・即時。
- Finalize:Dispose を呼び忘れたときの最後の保険。GC 任せ。
両方持つクラスでは、Dispose() の中で GC.SuppressFinalize(this) を呼んで「もうファイナライザは不要」と GC に伝えるのが定石です。具体的なコードパターンは次の記事で示します。
まとめ
| 項目 | 内容 |
|---|---|
| インターフェース | System.IDisposable / System.IAsyncDisposable |
| 中核メソッド | Dispose() / DisposeAsync() |
| 呼び出し方法 | using 文、using 宣言(C# 8.0+)、await using |
| Finalize との違い | 決定論的・即時・GC 負担なし |
| 基本ルール | 所有者が解放する。using で囲む |
Finalize() は GC 任せの「保険」、Dispose() は呼び出し側が握る「正規の解放経路」。普段書くべきはほぼ常に Dispose 側 です。次の記事では、Finalizable と Disposable の両方を備えた 完全な Dispose パターン を、サンプルコードとともに整理します。