みなさんこんにちは!ヒロポンです!
SqlConnection やファイルハンドルを自前で抱えるクラスを書いてて、ふと「これ、閉じ忘れたらリークするよな??」って手が止まったこと、ないですか。
業務系だと DB 接続を1本持つ小さなユーティリティを自作する場面、普通にありますよね。そいつを using で使えるようにしたい。要するに C# の IDisposable を実装する Dispose パターンを書きたい、という話です。
で、検索する。すると Dispose(bool disposing) だの GC.SuppressFinalize だの ~クラス名()(ファイナライザ)だのが一気に出てきて、「え、こんなに書くの?」ってなる。俺も最初これで身構えました。
先に結論。多くの業務クラスは、あの重いパターンを全部書く必要はない。抱えてるのがマネージドリソースだけなら単純版で足ります。ここでは Dispose パターンがなぜこの形なのかを、単純版 → フル版 → using の順で積み上げていきます。
忙しい人向けに最初にまとめ
- 抱えてるのがマネージドリソースだけ(
SqlConnectionなど、それ自体 IDisposable なオブジェクト)なら → 単純な Dispose で各リソースのDispose()を呼ぶだけ。ファイナライザもGC.SuppressFinalizeも要らない - 自分でアンマネージドリソース(ネイティブハンドル等)を確保している時だけ →
Dispose(bool)パターン + ファイナライザ +GC.SuppressFinalize - 使う側は
usingで包むだけ。ブロックを抜ける時に例外が出ても、自動でDisposeが呼ばれる - ハマりどころは「マネージドだけなのにファイナライザを書いて GC 負荷を増やす」パターンと「Dispose を呼び忘れて GC 任せにし、接続プールが枯渇する」パターン
コードは記事の中盤(単純版 → フル版)に全部あります。
そもそも IDisposable は何を解決するのか
C# の IDisposable は、もう使わないリソースを、GC を待たずに「今すぐ」確定的に解放するための仕組みです。定義はたった1メソッド。
public interface IDisposable
{
void Dispose();
}
なんでこんなものが要るのか。C# はメモリを GC(ガベージコレクタ)が自動で回収してくれる。ん?だったらメモリだけ見れば Dispose は要らないよな、って思いますよね。
引っかかるのはメモリ以外のリソースです。DB 接続、ファイルハンドル、ソケット、ロック。この手のやつは数に上限があったり、開いてる間ずっと他をブロックしたりする。GC がいつ回収するかは不定。「使い終わったら即閉じる」を GC 任せにはできないわけです。
だから .NET は約束を用意した。「後始末を持つオブジェクトは IDisposable を実装してね、使う側は責任を持って Dispose を呼んでね」。これが IDisposable の正体です。
ここまでで分かったこと:
IDisposableは「メモリ以外のリソースを確定的に閉じる」ための約束。中身はDispose()1個だけ。
パターン1: 単純な Dispose(マネージドリソースだけ)
まずはいちばん多いケース。自分のクラスが抱えてるのが、すでに IDisposable なマネージドオブジェクトだけのとき。
たとえば「DB 接続を1本持って、それ経由で書き込むだけ」のクラス。やることは「抱えてる子の Dispose を呼ぶ」だけです。
動く形で書くと、こんな感じになります(SqlConnection の代わりに、動作確認できるよう StringWriter をマネージドリソース役にしています)。
using System;
using System.IO;
class ManagedResourceHolder : IDisposable
{
private readonly StringWriter _writer; // マネージドリソース (それ自体 IDisposable)
private bool _disposed;
public ManagedResourceHolder()
{
_writer = new StringWriter();
Console.WriteLine("リソースを開いた");
}
public void Write(string text) => _writer.Write(text);
public void Dispose()
{
if (_disposed) return; // 二重 Dispose を無害化 (冪等にする)
_writer.Dispose(); // 抱えているマネージドリソースを閉じる
_disposed = true;
Console.WriteLine("リソースを閉じた");
}
}
class Program
{
static void Main()
{
using (var holder = new ManagedResourceHolder())
{
holder.Write("処理中...");
} // ここを抜けると自動で Dispose される
Console.WriteLine("using を抜けた");
}
}
実行結果:

ポイントは3つ。
- ファイナライザ(
~ManagedResourceHolder())は書いていない。抱えてるのがマネージドだけなら要らない GC.SuppressFinalizeも呼んでいない。ファイナライザが無いので、抑制する対象もないから_disposedフラグで二重呼び出しを無害化している。Disposeは何回呼ばれても壊れない、が定石
「え、これだけ?」って思うでしょ。これだけです。検索で出てくる重いパターンは、次の「アンマネージドを持つ場合」の話。業務で書く自作クラスの大半は、この単純版で足ります。
継承させる予定がないなら、いっそ sealed class ManagedResourceHolder にしておく。そうすると後で出てくる Dispose(bool) の仮想メソッド版すら考えなくてよくなります。いい感じにシンプルになる。
ここまでで分かったこと: マネージドリソースだけを抱えるなら、
Dispose()の中で子のDispose()を呼び、二重呼び出しをフラグでガードするだけ。ファイナライザは不要。
パターン2: Dispose(bool) パターン(アンマネージド + ファイナライザ)
次。自分で「アンマネージドリソース」を確保している場合です。ネイティブ API から取ったハンドル、Marshal.AllocHGlobal で確保したメモリ、みたいなやつ。
これらは GC が中身を知らない。だから「万が一 Dispose を呼び忘れられても、最後に GC のタイミングで解放したい」という保険がほしくなる。その保険がファイナライザです。
しかも、Dispose から呼ばれた時とファイナライザから呼ばれた時で、やっていい後始末が違う。この2経路を1メソッドにまとめたのが Dispose(bool disposing) です。
using System;
using System.IO;
class FullResource : IDisposable
{
private IntPtr _handle; // アンマネージドリソースの代役 (ネイティブハンドル等)
private StringWriter _managed; // マネージドリソース
private bool _disposed;
public FullResource()
{
_handle = new IntPtr(1); // 「ハンドルを確保した」体
_managed = new StringWriter();
}
// 使う側から呼ばれる公開 Dispose
public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this); // 後始末は済んだ。ファイナライザは走らせなくていい
}
// 実体。disposing で「誰から呼ばれたか」を分岐する
protected virtual void Dispose(bool disposing)
{
if (_disposed) return;
if (disposing)
{
// Dispose() 経由のときだけ、マネージドリソースに触ってよい
_managed?.Dispose();
Console.WriteLine("マネージド + アンマネージドを解放");
}
else
{
Console.WriteLine("アンマネージドだけ解放 (ファイナライザ経由)");
}
// アンマネージドは、どちらの経路でも必ず解放する
if (_handle != IntPtr.Zero) _handle = IntPtr.Zero;
_disposed = true;
}
// ファイナライザ。Dispose 呼び忘れの保険
~FullResource() => Dispose(false);
}
disposing の真偽で何が変わるのか。ここが一番の勘所です。
Dispose(true)(Dispose()から)→ 呼び出し元がまだ生きてる普通の後始末。マネージドもアンマネージドも両方閉じてよいDispose(false)(ファイナライザから)→ GC が回してる最中。マネージドオブジェクトはもう回収済みかもしれないので触らない。アンマネージドだけ解放する
なんでファイナライザからマネージドに触っちゃダメなのか?? ファイナライザが走る順番は保証されないからです。自分が抱えてる _managed が、自分より先にファイナライズされている可能性がある。そこに触ると未定義の挙動。だから disposing == false の側では、マネージドに一切手を出さない。
動かして両方の経路を見るとこうなります。
class Program
{
static void Main()
{
// (1) using 経由: Dispose(true) が走る
using (var r = new FullResource())
{
Console.WriteLine("using ブロック内");
}
// (2) わざと Dispose せず GC に回収させる: ファイナライザ = Dispose(false) が走る
MakeGarbage();
GC.Collect();
GC.WaitForPendingFinalizers();
Console.WriteLine("完了");
}
static void MakeGarbage()
{
var leaked = new FullResource(); // Dispose を呼ばずに捨てる
}
}
実行結果:

(1) は Dispose() から Dispose(true)、(2) は閉じ忘れをファイナライザが Dispose(false) で拾ってる。ちゃんと経路が分かれてる!!
ここまでで分かったこと: アンマネージドを持つ時だけ、ファイナライザで保険をかける。
disposingフラグで「マネージドに触っていいか」を分岐する。
GC.SuppressFinalize は何をしているのか
Dispose() の中に1行だけ出てきた GC.SuppressFinalize(this)。これをサラッと飛ばす記事が多いんだけど、業務SEがいちばん「?」ってなる行なので、ちゃんと言います。
ファイナライザを持つオブジェクトは、GC が1回では回収できない。いったんファイナライズ用のキュー(finalization queue)に載せ、専用スレッドでファイナライザを走らせ、その次の GC でようやく本体を回収する。つまり回収に GC が2回かかり、世代も1つ上がる。地味にコストです。
でも Dispose() を呼んだ時点で後始末はもう終わってる。だったらファイナライザを走らせる意味はない。そこで GC.SuppressFinalize(this) を呼んで、「こいつのファイナライザはもう要らないから、キューに載せないで」と GC に伝える。これで2回コレクションのコストが消える。
まとめるとこういう対応関係です。

だから理想は「毎回ちゃんと Dispose を呼んで、ファイナライザは一度も走らせない」。ファイナライザはあくまで最後の保険であって、常用ルートじゃない。ここを勘違いして「ファイナライザがあるから閉じ忘れても大丈夫」と思い込むと、後で出てくるハマりに直行します。
あと、パターン1(単純版)では GC.SuppressFinalize は呼ばなくていい。抑制すべきファイナライザがそもそも無いから。「Dispose には必ず SuppressFinalize を書く」と機械的に覚えると、単純版で無駄な1行を足すことになる。要るのはフルパターンの時だけです。
パターン3: using で自動的に閉じる(使う側の話)
ここまではクラスを作る側の話でした。作ったクラスを使う側は、using で包むだけです。
using はコンパイル時に try / finally へ展開されます。手で書くと抜け漏れるやつを、コンパイラが確実にやってくれる。

after 側が裏でやってることを図にすると、こう。

例外が飛んでも finally で必ず Dispose される。これが using の効きどころです。手動の try / catch だと、途中で return したり別の例外が重なったりで閉じ忘れる。あなたも早期 return で finally を素通りさせた経験、ありませんか。using ならそこを気にしなくていい。
ちなみに C# 8.0 からは、ブロックすら書かない using 宣言も使えます。
using var conn = new SqlConnection(connStr);
conn.Open();
// ... 処理 ...
// メソッド (スコープ) を抜ける時に自動で Dispose
スコープ末尾で Dispose されるので、ネストが浅くなって、いい感じに読める。ただし using var は C# 8.0 以降の構文。C# 7.3 のままの現場では従来の using (...) { } ブロックを使ってください。使い分けの詳しい話は「C# using の3形態」の記事にまとめてあります。
ここまでで分かったこと: 作る側は
IDisposableを実装、使う側はusingで包む。using は try / finally 展開なので、例外時も確実に閉じる。
適用判断 — 結局どれを書けばいいのか
3つ出てきたので、いつどれを使うのか一枚で整理します。

言いたいのはこれ。業務で自作するクラスの大半は「単純 Dispose」で終わる。SqlConnection も StreamReader も HttpClient も、中でアンマネージドを抱えて後始末してくれるのは .NET 側の仕事。俺たちが書くラッパークラスは、その子たちの Dispose を呼ぶだけでいい。
自分でネイティブハンドルを P/Invoke で握る……みたいな低レベルなことをして初めて、フルパターンの出番になる。業務系でそこまでやる頻度は、正直かなり低いです。
俺の現場ではこう踏んだ
概念は分かった。で、ちゃんと閉じないと実際に何が起きるのか。ここは俺がやらかした話をします。
流通系の基幹保守をやってた頃、夜間バッチ用に DB 接続を持つ小さなヘルパークラスを自作した。IDisposable は実装せず、コンストラクタで Open、あとは呼び出し側が適当なタイミングで捨てる作り。今思うと雑でした。
これが数日は普通に動くんですよ。で、忘れた頃に夜間バッチが「接続プールがいっぱいです」系のエラーで落ちる。接続プールの上限は既定で100。閉じてない接続がじわじわ溜まって、ある晩ついに上限を超えた。原因にたどり着くまで半日、ログとにらめっこ。落ちた翌朝に上長から「昨夜のバッチ止まってるけど」と声をかけられて、血の気が引いたやつです。
犯人は単純で、接続を GC 任せにしていたこと。GC はメモリが逼迫するまで回さないから、接続はずっと開いたまま。ファイナライザすら書いてなかったので保険もゼロ。打つ手なしでした。
直したのは2箇所だけ。ヘルパーに IDisposable を実装して各接続の Dispose を呼ぶようにし、呼び出し側を全部 using で包んだ。それ以降、プール枯渇は一度も出ていません。
教訓としては、「閉じる」を人間の意志に頼らない。using で構文に閉じてもらう。この記事の単純版パターンで十分に防げた事故でした。
まとめ
Dispose パターンがなぜあの形なのか、積み上げで見てきました。
IDisposableは「メモリ以外のリソースを確定的に閉じる」約束。中身はDispose()1個- 抱えてるのがマネージドだけなら → 単純 Dispose(子の Dispose を呼ぶ + 冪等ガード)。ファイナライザも SuppressFinalize も要らない
- 自分でアンマネージドを持つ時だけ →
Dispose(bool)フルパターン + ファイナライザ +GC.SuppressFinalize disposingフラグは「マネージドに触っていい経路か」を分ける印- 使う側は
usingで包む。例外時も確実に閉じる
要は、検索で最初に出てくる重いパターンに気圧される必要はないってことです。大半の業務クラスは単純版で足りるし、using で閉じる習慣さえ付けば、リソースリークは目に見えて減る。定石を定石として実装しておくだけで、夜間バッチが接続枯渇で落ちる、みたいな地味に痛い事故が現場から消えます。そこが Dispose パターンの一番の効きどころです。
よくある質問
マネージドリソースだけのクラスにファイナライザは要りますか
要りません。むしろ書かない方がいい。ファイナライザを付けると、そのオブジェクトは回収に GC が2回かかるようになり、GC 負荷が増えます。抱えてるのが IDisposable なマネージドオブジェクトだけなら、単純 Dispose でそれぞれの Dispose() を呼ぶだけで十分です。
Dispose を2回呼んでしまったら例外になりますか
きちんと実装されていれば、なりません。_disposed フラグで2回目以降を無害化する(冪等にする)のが定石だからです。逆に、Dispose 済みのオブジェクトの通常メソッドを呼んだ場合は ObjectDisposedException を投げるのが行儀のいい作りになります。
using を使えば Dispose の実装は不要ですか
役割が別です。using は使う側の書き方で、既に IDisposable を実装済みのクラスを自動で閉じてくれます。自分で後始末が要るクラスを作る側は、IDisposable を実装して Dispose() を書く必要があります。作る側と使う側、両方の話だと思ってください。
GC.SuppressFinalize を単純版でも呼んだ方が安全ですか
安全性は変わりません。単純版にはファイナライザが無いので、抑制する対象がなく、呼んでも実質何も起きません。「Dispose には必ず書くもの」と機械的に覚えると混乱するので、ファイナライザを書いた時だけセットで呼ぶ、と理解しておくのが正確です。
動作確認メモ
この記事の C# コード(単純 Dispose / Dispose(bool) フルパターン / using デモ)はコンテナ上の .NET でコンパイル・実行し、記載の出力を確認しています。ネイティブハンドルは動作確認のため IntPtr の代役で表現しているので、実際のアンマネージド解放処理(CloseHandle 等)はお使いの環境に合わせて差し替えてください。
次に読むべき記事





以上!
同じ「閉じ忘れ」で夜を溶かした人がいたら、どんどんシェア待ってるぜ!!
執筆者
バイブス父さん — 業務 SE 7 年 (SIer 正社員 2 / フリーランス 5)。 現職は SEO 直轄部の AI アドバイザー兼 PL、 副業で中小 SIer の CTO。 SIer の正社員からフリーランスに転じ、 複数のエージェント経由で案件を回してきた経験ベースで「業務 SE 視点」 の技術 + キャリア記事を書いています。
🐦 X: @hiro_progra0524 (日々の現場メモ更新中)
📝 About Me で経歴詳細を見る




