0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

R11 【掟・判例】画像とファイルが掴まれたままを終わらせる ── IDisposableとusing / 後始末の担当を決める / Finalizeは保険 / SafeHandleでハンドルを守る

Last updated at Posted at 2026-01-19

連載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=falseDispose 終了時に必ず通す

3. 結論(揃えるのはこれだけ)

狙い: 実装の前に、判断基準を3点へ圧縮して迷いを減らす。

  1. using で短命にする
  2. 後始末の担当(最後に Dispose する側)を決める
  3. 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=falseDispose

結論: NotifyIconIDisposable。表示を消してから閉じる。

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 を差し替え続ける
  • FontBrush を作り捨てせず溜める
  • 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();
// 描画

ポイント:

  • Graphicsusing で短命に寄せる
  • 描画は 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=falseDispose トレイが残る NotifyIcon終了処理
Win32 IntPtr 公開を避ける 例外経路で漏れる P/Invoke境界

7-2. OK/NG(差が出るところだけ)

狙い: 判断が揺れやすい所だけ、短い対比で揃える。

観点 OK例 NG例 理由(壊れ方) レビューで見る所
片付け担当 new した側が Dispose 受け渡しが曖昧 漏れ/二重解放 生成箇所と責務
短命化 using で閉じる finally無しでreturn 後始末漏れ using / try-finally
差し替え 古い ImageDispose 上書きだけ GDIが溜まる 差し替え処理
Finalize 保険として最小 通常経路の代替 解放が遅れる Finalizeの目的
ハンドル SafeHandle で保持 IntPtr を公開 管理が崩れる P/Invoke境界

7-3. レビュー観点(指摘コメント例)

狙い: 方向性だけを揃え、手戻りを短くする。

観点 ありがちな見落とし 壊れ方 指摘コメント例(直球禁止)
using/finally 例外経路で閉じない ファイルが掴まれたまま 例外経路でも閉じる形へ寄せたい
担当 誰が Dispose するか不明 漏れ/二重解放 片付け担当が読める形へ揃えたい
画像差し替え Image 上書きだけ 重くなる/掴む 差し替え前の解放が入っているか確認したい
SafeHandle IntPtr を直接扱う タイミング依存で不安定 ハンドルを SafeHandle へ寄せたい
長命資源 Timer/CTS放置 背景動作が残る 止めて閉じる手順を揃えたい

8. セルフチェック(5問)

狙い: その場で判定できる問いにして、担当と寿命の曖昧さを減らす。

  1. その IDisposable は短命(using)か、長命(担当が Dispose)か
  2. new した場所と、Dispose する場所が同じ担当になっているか
  3. 注入(DI)で受け取ったものを勝手に閉じていないか
  4. 画像差し替えで「差し替え前」を閉じているか
  5. IntPtr を返して担当を散らしていないか(SafeHandle に寄っているか)
回答例
  1. メソッド内で完結するなら using。フィールドで保持するならその型が IDisposable を実装して後始末の担当になる。
  2. new した側が閉じる原則に寄せる。返り値で渡す場合は Open/Create 命名とコメントで担当が移ることを明示する。
  3. DI注入は借り物になりやすい。借り物は閉じない前提へ寄せ、寿命は注入元(コンテナ)で管理する。
  4. PictureBox.Image は上書きだけだと古い Image が残る。差し替え前を退避して差し替え後に Dispose する手順へ寄せる。
  5. IntPtr 公開は例外経路で漏れやすい。SafeHandle を返し、using で寿命を閉じる形へ寄せる。

関連リンク

狙い: 寿命管理の周辺トピックへ繋ぎ、同じ落ち方を別角度から潰せるようにする。


0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?