連載Index(読む順・公開済(リンク)はここが最新): S00_門前の誓い_総合Index
削除できない。上書きできない。リネームできない。
「別のプロセスが使用中です」で止まる。
再起動で逃げると直るが、しばらくするとまた出る。
原因は「後始末の担当」が曖昧で、OS資源(ファイル/GDI/ハンドル)が残ることが多い。
このページは IDisposable / using / Finalize / SafeHandle をまとめて扱い、漏れにくい構造へ寄せる。
1. このページで手に入るもの(最短)
狙い: スクロールの価値を先に確定させ、貼れる形を最初に出す。
- 「短命は
using、長命は担当を決めてDispose」の最小ルール - 症状→原因→対策の逆引き表(最初に見る表)
- 最短テンプレ(コピペ): Stream/HttpClient/WinForms画像差し替え/NotifyIconの後始末
-
Finalizeの役割(保険)と、通常経路で頼らない理由 -
IntPtr直運用を避け、SafeHandleに寄せる理由と最小例 - 判例: IO/画像/トレイ/DI/継承/Win32の混入点を上から潰す
2. 先に逆引き(症状→原因→最短対処)
狙い: 症状から最短で“見る場所”へ飛び、切り分けの距離を短くする。
| 症状 | ありがちな原因 | 切り分け(見る場所) | 最短の対処 | 再発防止(ルール化) |
|---|---|---|---|---|
| 画像差し替えで重くなる |
Bitmap/Image の後始末漏れ |
差し替え箇所 | 差し替え前を Dispose
|
画像差し替え手順を統一 |
| ファイルが開けない(掴んだまま) |
Stream/Reader の後始末漏れ |
return/例外経路 |
using へ寄せる |
IOは using 必須 |
| 通信が不安定 | 寿命が曖昧(長命なのに放置) |
new の位置 / DI |
担当を決めて後始末 | DIと寿命方針を統一 |
| たまに不安定 | ハンドル管理が曖昧 |
IntPtr 運用 |
SafeHandle へ寄せる |
IntPtr 公開を避ける |
| トレイが残る |
NotifyIcon の後始末漏れ |
終了経路 |
Visible=false→Dispose
|
終了時に必ず通す |
3. 結論(揃えるのはこれだけ)
狙い: 実装の前に、判断基準を3点へ圧縮して迷いを減らす。
-
usingで短命にする - 後始末の担当(最後に
Disposeする側)を決める -
Finalizeは保険(通常経路の解放手段にしない)
4. 最短テンプレ(コピペ)
狙い: まず「貼る場所」と「担当」をコメントで見える形にし、漏れにくい型へ寄せる。
4-1. 短命(メソッド内で閉じる):using var
結論: ローカルで作った資源は、そのスコープで閉じる。
// ポイント: IOは短命に寄せる(例外/return経路でも閉じる)
using var stream = File.OpenRead(path); // ここで開く
using var reader = new StreamReader(stream); // ここで包む
var text = reader.ReadToEnd(); // ここで使う
// スコープ末尾で自動的に Dispose(閉じ忘れを減らす)
4-2. 長命(メンバーで持つ):その型が Dispose する
結論: フィールドで保持するなら、その型が後始末の担当になる。
public sealed class Worker : IDisposable
{
// ポイント: メンバーで保持 = この型が後始末の担当
private readonly HttpClient _client = new();
private bool _disposed;
public Task<string> GetAsync(string url)
=> _client.GetStringAsync(url);
public void Dispose()
{
if (_disposed) return; // ポイント: 多重呼び出しに耐える
_client.Dispose(); // ポイント: 担当がここで閉じる
_disposed = true;
}
}
4-3. WinForms(画像差し替え):差し替え前を Dispose
結論: PictureBox.Image の上書きだけでは古い Image が残る。
// ポイント: 差し替え前を退避して、差し替え後に閉じる
var old = pictureBox1.Image;
pictureBox1.Image = new Bitmap(path); // ここで新しい画像を作る
old?.Dispose(); // ここで古い画像を閉じる(掴みっぱなしを減らす)
4-4. WinForms(トレイ):Visible=false→Dispose
結論: NotifyIcon は IDisposable。表示を消してから閉じる。
private NotifyIcon? _notify;
private void CleanupNotifyIcon()
{
if (_notify is null) return;
_notify.Visible = false; // ポイント: 先に表示を消す(トレイ残りを減らす)
_notify.Dispose(); // ポイント: 後始末
_notify = null; // ポイント: 再利用の誤りを避ける
}
5. 解説:定義→評価規則→型の変化→落とし穴
狙い: using を貼るだけで終わらせず、「何が漏れるか」「担当の決め方」「崩れるポイント」を揃える。
5-1. 定義:何が漏れると困るのか(メモリ以外の資源)
結論: GCが回収するのはマネージドメモリ。困るのはOS側に作られる資源が残ること。
GCが回収するのはマネージドメモリ。
困るのは、OS側に作られる資源が残ること。
- ファイルやソケットのOS資源(ファイルが掴まれたまま、接続が増え続ける)
- 画像やフォント描画のOS資源(画像差し替えで重くなる)
この手の資源は、Dispose を呼ばないと解放が遅れたり、残ったりする。
5-2. 定義:GDIとは何か(画像やフォントの裏側)
狙い: 「メモリは増えていないのに重い」の正体を先に揃える。
GDI(Windowsの描画系)は、画像・文字・線を描くためのOS機能。
.NET 側の型(例: Bitmap, Font, Brush, Pen, Graphics)は、内部でOS側の描画資源を持つことがある。
-
Bitmapを差し替え続ける -
FontやBrushを作り捨てせず溜める -
Graphicsを閉じずに使い回す
だから「メモリは増えていないのに重い」が起きる。
5-3. 定義:ハンドルとは何か(識別子ではなくOS資源の参照)
狙い: ハンドル数が増え続けると何が起きるかを先に揃える。
ハンドルは、OSが管理している資源(ファイル、イベント、ミューテックス、ウィンドウ等)に紐づく参照。
単なる番号ではなく、OS側の資源に結びついている。
- 作る: ハンドル数が増える
- 捨てない: ハンドル数が戻らない
- 増え続ける: 新規作成や操作が失敗し始める
5-4. 評価規則:後始末の担当(最後に Dispose する側)を決める
狙い: 漏れと二重解放の両方を減らすため、担当の線引きを先に置く。
結論: 片付けるのは誰かを決める。曖昧だと漏れか二重解放に寄る。
原則はこの3つ。
-
newした側が片付ける - メンバーとして保持するなら、その型が片付ける
- 引数で受け取ったものは基本的に片付けない(借り物扱い)
5-4-1. 返り値で渡すときは、契約が読める名前へ寄せる
狙い: 片付け担当が呼び出し側へ移る形を、名前で見える化する。
// ポイント: 呼び出し側が Dispose する前提(担当が呼び出し側へ渡る)
public static FileStream OpenLogStream(string path)
=> new FileStream(path, FileMode.Append, FileAccess.Write, FileShare.Read);
GetXxx() のような曖昧名は避ける方が止まりにくい。
5-5. 型の変化:短命を using で閉じる(2つの書き方)
狙い: チーム内で揺れやすい using の書き方を揃え、閉じ忘れを減らす。
5-5-1. using宣言(スコープ末尾で閉じる)
// ポイント: スコープ末尾で自動的に Dispose
using var stream = File.OpenRead(path);
using var reader = new StreamReader(stream);
var text = reader.ReadToEnd();
5-5-2. usingブロック(ブロック末尾で閉じる)
// ポイント: ブロック末尾で Dispose(早く閉じたい場合に有効)
using (var stream = File.OpenRead(path))
using (var reader = new StreamReader(stream))
{
var text = reader.ReadToEnd();
}
5-6. 型の変化:短命 / 長命 / 借り物(DI)
狙い: 同じ IDisposable でも寿命が違うため、扱いを分類して迷いを減らす。
| パターン | 寿命 | 片付け担当 | 書き方 | 典型例 |
|---|---|---|---|---|
| 短命ローカル | メソッド内 | そのメソッド | using var |
Stream/Reader/Graphics |
| 長命メンバー | クラス寿命 | そのクラス |
IDisposable 実装 |
Timer/CTS/Bitmap保持 |
| 借り物注入(DI) | 外部寿命 | 注入元 |
Dispose を持たない |
注入されたHttpClient |
| 返り値で受け渡し | 呼び出し側寿命 | 呼び出し側 |
Open/Create 命名 |
OpenLogStream() |
| ネイティブ資源 | OS寿命 | SafeHandle | SafeHandleで保持 | Win32 handle |
5-6-1. DI(依存性注入)で受け取る場合(借り物になり得る)
狙い: new していないものを勝手に閉じないルールを明示する。
DI(Dependency Injection, 依存性注入)は、必要なものを new せず、外から渡してもらう作り。
このとき、寿命の管理は「渡してくる側」が持っていることがある。
public sealed class Worker
{
private readonly HttpClient _client;
// ポイント: 外から渡される = ここでは new していない
public Worker(HttpClient client) => _client = client;
// ポイント: Dispose を持たない(担当は注入元)
}
5-7. 型の変化:Disposeパターンが必要になる場面(継承を許す型で資源を持つ)
狙い: 片付けの連鎖を崩さないための骨格を揃える。
典型例:
- ベースクラスが
Timer/CancellationTokenSourceを持つ - ベースクラスが
SafeHandleやアンマネージ資源を持つ - 派生クラスが
Bitmap/FileStreamを追加する
この形では、片付けの連鎖を崩さないために Dispose(bool disposing) を使う。
public abstract class ResourceOwner : IDisposable
{
private bool _disposed;
protected virtual void Dispose(bool disposing)
{
if (_disposed) return;
if (disposing)
{
// ポイント: managed資源はここで片付ける(派生もここへ寄せる)
}
// ポイント: unmanaged資源があるならここで片付ける
_disposed = true;
}
public void Dispose()
{
Dispose(disposing: true);
GC.SuppressFinalize(this); // ポイント: Finalize を抑制(通常経路で片付いた印)
}
~ResourceOwner()
{
// ポイント: 保険(いつ走るかは保証されない)
Dispose(disposing: false);
}
}
5-8. 落とし穴:Finalizeは保険(通常経路の解放手段にしない)
狙い: 「いつ片付くか分からない」ことを先に揃え、通常経路で頼らない設計へ寄せる。
Finalizeは、Dispose し忘れても、いつかは片付くための保険。
ただし、いつ実行されるかは保証されない。通常経路の片付けに使わない。
5-9. 落とし穴:IntPtr 直運用を避け、SafeHandle へ寄せる
狙い: 例外経路で漏れやすい境界を、型で安全側へ倒す。
IntPtr を直接扱う運用は、例外経路で漏れやすい。
SafeHandle は、OS資源の後始末を安全側に倒して管理する仕組み。
6. 判例:混入点が多い順に潰す
狙い: 実際に踏み抜きやすい順に、悪い例→直す→ポイントで揃える。
6-1. IO:Stream/Reader を閉じない(例外/return経路で漏れる)
狙い: IOは短命に寄せ、例外/return経路でも閉じる形へ寄せる。
悪い例:
var stream = File.OpenRead(path);
var reader = new StreamReader(stream);
var text = reader.ReadToEnd();
return text; // ポイント: ここで閉じない経路が生まれる
直す:
// ポイント: using に寄せると例外/return経路でも閉じる
using var stream = File.OpenRead(path);
using var reader = new StreamReader(stream);
var text = reader.ReadToEnd();
return text;
ポイント:
- IOは
usingへ寄せる - 例外/return経路で閉じない形を避ける
- スコープ末尾で閉じるため、担当が曖昧になりにくい
6-2. WinForms:PictureBox.Image を上書きだけで差し替える
狙い: 差し替え前を閉じる手順を揃え、GDI資源が残る形を減らす。
悪い例:
pictureBox1.Image = new Bitmap(path); // ポイント: 古いImageが残る
直す:
// ポイント: 差し替え前を退避して、差し替え後に閉じる
var old = pictureBox1.Image;
pictureBox1.Image = new Bitmap(path);
old?.Dispose();
ポイント:
- 差し替え前を退避する
- 差し替え後に
Disposeする - 画像差し替え手順を統一すると漏れが減る
6-3. WinForms:CreateGraphics() を閉じずに使う
狙い: Graphics は短命に寄せ、スコープで閉じる形へ寄せる。
悪い例:
var g = this.CreateGraphics();
// 描画
// ポイント: g を閉じない経路が生まれる
直す:
// ポイント: Graphics は using で短命へ寄せる
using var g = this.CreateGraphics();
// 描画
ポイント:
-
Graphicsはusingで短命に寄せる - 描画は
OnPaintへ寄せるのが筋(必要な場合のみCreateGraphics) - 使い回し前提の保持は避ける
6-4. WinForms:NotifyIconがトレイに残る
狙い: 終了経路(メニュー終了/フォーム終了/例外時の片付け)で同じ後始末を必ず通す。
悪い例:
_notify?.Dispose(); // ポイント: Visible が残ることがある
直す:
// ポイント: 先に消してから閉じる
if (_notify is not null)
{
_notify.Visible = false;
_notify.Dispose();
_notify = null;
}
ポイント:
-
Visible=falseを先に行う -
Disposeで後始末 - 終了経路が複数あっても同じ片付けを通す
6-5. 長命メンバー:HttpClient/Timer/CTS を放置する
狙い: メンバーで保持したら、その型が担当になり Dispose で閉じる。
悪い例:
public sealed class Worker
{
private readonly HttpClient _client = new();
// ポイント: Dispose が無いと担当が曖昧になる
}
直す:
public sealed class Worker : IDisposable
{
// ポイント: 長命メンバー = この型が担当
private readonly HttpClient _client = new();
private readonly CancellationTokenSource _cts = new();
private bool _disposed;
public void Dispose()
{
if (_disposed) return;
_cts.Dispose(); // ポイント: 背景動作を止めて閉じる
_client.Dispose(); // ポイント: ここで閉じる
_disposed = true;
}
}
ポイント:
- メンバー保持は担当がこの型になる
-
Timer/CTSは「止めて閉じる」まで含めて担当 - 多重
Disposeに耐える形へ寄せる
6-6. DI:注入された IDisposable を勝手に閉じる
狙い: 借り物は閉じない。寿命は注入元が持つ前提へ寄せる。
悪い例:
public sealed class Worker : IDisposable
{
private readonly HttpClient _client;
public Worker(HttpClient client) => _client = client;
public void Dispose() => _client.Dispose(); // ポイント: 借り物を閉じてしまう可能性
}
直す:
public sealed class Worker
{
private readonly HttpClient _client;
public Worker(HttpClient client) => _client = client;
// ポイント: 借り物はここで閉じない(担当は注入元)
}
ポイント:
-
newしていないものは閉じない前提へ寄せる - DIコンテナ側で寿命を管理する設計に揃える
- 例外として「工場で作って渡す」形なら契約(名前/コメント)で明示する
6-7. Win32:IntPtr を直接扱う(例外経路で漏れる)
狙い: IntPtr を直接返さず、SafeHandle に寄せて安全側へ倒す。
悪い例:
// ポイント: IntPtr を返すと後始末の担当が散りやすい
public static IntPtr OpenHandle(string path) => /* CreateFile */ IntPtr.Zero;
直す(最小例):
using Microsoft.Win32.SafeHandles;
using System;
using System.Runtime.InteropServices;
internal static class Native
{
[DllImport("kernel32.dll", SetLastError = true, CharSet = CharSet.Unicode)]
private static extern SafeFileHandle CreateFile(
string lpFileName,
uint dwDesiredAccess,
uint dwShareMode,
IntPtr lpSecurityAttributes,
uint dwCreationDisposition,
uint dwFlagsAndAttributes,
IntPtr hTemplateFile);
public static SafeFileHandle OpenHandle(string path)
{
const uint GENERIC_READ = 0x80000000;
const uint FILE_SHARE_READ = 0x00000001;
const uint OPEN_EXISTING = 3;
var h = CreateFile(path, GENERIC_READ, FILE_SHARE_READ, IntPtr.Zero, OPEN_EXISTING, 0, IntPtr.Zero);
if (h.IsInvalid) throw new InvalidOperationException("CreateFile failed.");
return h; // ポイント: SafeHandle が後始末を持つ
}
}
// ポイント: using で寿命を閉じる(例外/return経路でも閉じる)
using var handle = Native.OpenHandle(path);
// ここで利用
ポイント:
-
IntPtrを公開せずSafeHandleを返す -
usingで寿命を閉じる - 例外経路でも後始末が走る形へ寄せる
7. チェックリスト:レビューで見る所
狙い: 片付け担当/寿命/例外経路の3点を表で揃え、見落としを減らす。
7-1. 最短チェック(持ち帰り用)
結論: using / 担当 / 借り物 / 画像差し替え / トレイ / SafeHandle を揃える。
| 観点 | 確認ポイント | よくあるズレ | 見る場所 |
|---|---|---|---|
| using | IO/Graphics が using へ寄っている |
return/例外経路で漏れる | IO/描画箇所 |
| 担当 |
new した側が閉じる |
担当が曖昧 | 生成箇所と責務 |
| 長命 | メンバー保持は Dispose 実装 |
放置して残る | Worker/Service等 |
| 借り物(DI) | 注入されたものは閉じない | 借り物を閉じる | コンストラクタ注入 |
| 画像差し替え | 差し替え前を Dispose
|
上書きだけ | PictureBox/Image |
| トレイ |
Visible=false→Dispose
|
トレイが残る | NotifyIcon終了処理 |
| Win32 |
IntPtr 公開を避ける |
例外経路で漏れる | P/Invoke境界 |
7-2. OK/NG(差が出るところだけ)
狙い: 判断が揺れやすい所だけ、短い対比で揃える。
| 観点 | OK例 | NG例 | 理由(壊れ方) | レビューで見る所 |
|---|---|---|---|---|
| 片付け担当 |
new した側が Dispose
|
受け渡しが曖昧 | 漏れ/二重解放 | 生成箇所と責務 |
| 短命化 |
using で閉じる |
finally無しでreturn | 後始末漏れ |
using / try-finally |
| 差し替え | 古い Image を Dispose
|
上書きだけ | GDIが溜まる | 差し替え処理 |
| Finalize | 保険として最小 | 通常経路の代替 | 解放が遅れる | Finalizeの目的 |
| ハンドル |
SafeHandle で保持 |
IntPtr を公開 |
管理が崩れる | P/Invoke境界 |
7-3. レビュー観点(指摘コメント例)
狙い: 方向性だけを揃え、手戻りを短くする。
| 観点 | ありがちな見落とし | 壊れ方 | 指摘コメント例(直球禁止) |
|---|---|---|---|
| using/finally | 例外経路で閉じない | ファイルが掴まれたまま | 例外経路でも閉じる形へ寄せたい |
| 担当 | 誰が Dispose するか不明 |
漏れ/二重解放 | 片付け担当が読める形へ揃えたい |
| 画像差し替え |
Image 上書きだけ |
重くなる/掴む | 差し替え前の解放が入っているか確認したい |
| SafeHandle |
IntPtr を直接扱う |
タイミング依存で不安定 | ハンドルを SafeHandle へ寄せたい |
| 長命資源 | Timer/CTS放置 | 背景動作が残る | 止めて閉じる手順を揃えたい |
8. セルフチェック(5問)
狙い: その場で判定できる問いにして、担当と寿命の曖昧さを減らす。
- その
IDisposableは短命(using)か、長命(担当がDispose)か -
newした場所と、Disposeする場所が同じ担当になっているか - 注入(DI)で受け取ったものを勝手に閉じていないか
- 画像差し替えで「差し替え前」を閉じているか
-
IntPtrを返して担当を散らしていないか(SafeHandleに寄っているか)
回答例
- メソッド内で完結するなら
using。フィールドで保持するならその型がIDisposableを実装して後始末の担当になる。 -
newした側が閉じる原則に寄せる。返り値で渡す場合はOpen/Create命名とコメントで担当が移ることを明示する。 - DI注入は借り物になりやすい。借り物は閉じない前提へ寄せ、寿命は注入元(コンテナ)で管理する。
-
PictureBox.Imageは上書きだけだと古いImageが残る。差し替え前を退避して差し替え後にDisposeする手順へ寄せる。 -
IntPtr公開は例外経路で漏れやすい。SafeHandleを返し、usingで寿命を閉じる形へ寄せる。
関連リンク
狙い: 寿命管理の周辺トピックへ繋ぎ、同じ落ち方を別角度から潰せるようにする。
- シリーズ総合Index(読む順・公開済リンクが最新)
- R07 【掟・判例】WinForms寿命管理
- E06 【現場救急Tips】例外ログが残らず突然終了する(P/Invoke/WndProc/Handle寿命)