1
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?

R07:【掟・判例】WinForms寿命管理 閉じたのにまだプロセスがいるじゃねぇか ― ライフサイクル Dispose

Last updated at Posted at 2026-01-18

R07:【掟・判例】WinForms寿命管理 閉じたのにまだプロセスがいるじゃねぇか ― ライフサイクル Dispose

連載Index(読む順・公開済(リンク)はここが最新): S00_門前の誓い_総合Index

アプリを閉じたつもりが、もう一度起動すると二重起動チェックで弾かれたり、
タスクマネージャを見るとプロセスが残っていたり、メモリは使用中のままだったりする。
(まれに) Visual Studioのデザイナ(デザイン画面)を開くだけで落ちる。

抜けやすいのは、Load/Shown/FormClosing/FormClosed/Disposeに「始める」「止める」「外す」を置く場所が揃っていないこと。
このページでは、ライフサイクルごとの役割を分け、開始と停止を対にし、回収はDisposeへ寄せる。

このページのゴール

  • WinFormsのライフサイクル(Load/Shown/Closing/Closed/Dispose)の役割を分けて説明できる
  • 初期化をどこに置くと後で困りにくいか判断できる
  • CloseとDisposeの関係が説明できる
  • タイマー、イベント購読、バックグラウンド処理の止め所を決められる
  • カスタムコントロールでDisposeの書き方を揃えられる
  • デザイナで落ちる要因を避けられる(.NET 8/Framework 4.8どちらでも)

このページの登場人物(最低限)

  • ライフサイクルイベント: Load/Shown/FormClosing/FormClosed/Dispose
  • Dispose: 管理対象外リソースではなくても「やめる」「外す」「解放する」をまとめて行う出口
  • components(IContainer): デザイナが生成する部品箱。Disposeで中身が破棄される
  • デザイナ: Visual Studioのデザイン画面。実行時とは文脈が違う

ライフサイクル早見表(役割の切り分け)

タイミング 典型的に起きること 置く処理の目安 置かない方がよい処理
コンストラクタ フィールド初期化、InitializeComponent 依存注入、軽い初期値、イベントハンドラ接続(必要最小) 外部I/O、重い初期化、画面依存のレイアウト確定
Load 画面が生成され、表示直前 画面データの読み込み準備、UI初期状態の反映(軽量) 長時間処理、ブロッキングI/O
Shown 初回表示が終わる 重い初期化の開始、非同期開始、初回フォーカス調整 UIスレッドを塞ぐ処理
FormClosing 閉じる直前(キャンセル可能) 未保存確認、停止要求の発行、キャンセル判断 重い破棄処理、待ち合わせで固める
FormClosed 閉じた後 タイマー停止、イベント解除、バックグラウンド終了合図 UI操作(もう画面は無い)
Dispose 破棄(回収) components破棄、購読解除、Timer/Handle破棄 UIの状態参照(破棄順序で揺れる)

補足として、Disposeは「Closeの代わり」ではない。
Closeは閉じる導線、Disposeは資源回収の導線になる。

参考: Form.Close / Control.Dispose

掟: 寿命の区切りを決め、開始と停止を対にする

  • 掟1: 重い初期化はコンストラクタに置かない
    理由: デザイナ文脈でも実行される経路があり、設計時例外の原因になる。表示まで遅れ、画面が固まったように見えることもある。

  • 掟2: 表示後に始める処理はShownへ寄せる
    理由: 初回描画を先に通すと、UIの反応が残る。Loadに詰め込むと、表示が遅れた原因の切り分けが面倒になる。

  • 掟3: 終了は2段階(停止要求→回収)で分ける
    理由: FormClosingはキャンセル判断の場で、回収の場ではない。停止要求だけ出し、回収はFormClosed/Disposeで行うと順序が読みやすい。

  • 掟4: 画面の外に生きるもの(タイマー/購読/Task)は必ず止め所を持つ
    理由: 画面が閉じても動き続けると、プロセス残留、メモリ未回収、例外ログ増加につながる。

  • 掟5: Disposeでは「破棄するものの集合」を揃える
    理由: 破棄漏れは発生箇所が散りやすい。Disposeに寄せると探す場所が1つになる。

どう困るのか(よくある形)

形1: 閉じたのにプロセスが残る

  • タイマーが生きている
  • バックグラウンドスレッドが止まっていない
  • NotifyIconなど画面外の部品が生きている

対処は「止め所を決める」だけになる。FormClosedかDisposeに寄せる。

形2: デザイナで落ちる

  • コンストラクタで設定ファイルを読む
  • コンストラクタでDIコンテナを引く
  • コントロール生成時にDBやネットワークへ触れる

設計時は「画面の形を作る」だけの文脈で、実行時前提の処理が混ざると落ちやすい。
処理の置き場所をShown/Loadへ移すか、設計時判定で分岐する。

形3: 閉じた後にUIを触って例外になる

  • Shownで開始した非同期処理が、終了後にUI更新しようとする
  • Close後にTimer Tickが走り続ける

止め所(キャンセル/停止/購読解除)が無いのが原因になる。

最短の実装パターン(揃える型)

パターンA: Shownで開始し、FormClosedで止める(画面の寿命に合わせる)

private readonly System.Windows.Forms.Timer _timer = new();
private CancellationTokenSource? _cts;

protected override void OnShown(EventArgs e)
{
    base.OnShown(e);

    _cts = new CancellationTokenSource();

    _timer.Interval = 500;
    _timer.Tick += Timer_Tick;
    _timer.Start();

    _ = StartBackgroundAsync(_cts.Token);
}

protected override void OnFormClosed(FormClosedEventArgs e)
{
    _timer.Stop();
    _timer.Tick -= Timer_Tick;

    _cts?.Cancel();
    _cts?.Dispose();
    _cts = null;

    base.OnFormClosed(e);
}

private void Timer_Tick(object? sender, EventArgs e)
{
    // UI更新など
}

private async Task StartBackgroundAsync(CancellationToken ct)
{
    // 長い処理はキャンセル可能にする
    await Task.Delay(1000, ct);
}

ポイントは「開始と停止が対」になっていること。
止める場所が決まると、調査はそこだけ見ればよくなる。

パターンB: Disposeに回収を寄せる(探す場所を1つにする)

FormやControlはDisposeを持つ。回収の中心はここに置ける。

protected override void Dispose(bool disposing)
{
    if (disposing)
    {
        // イベント解除、Timer停止、購読解除など
        // componentsはデザイナが生成する場合がある
        components?.Dispose();
    }
    base.Dispose(disposing);
}

設計時安全(デザイナで落ちないための作法)

  • コンストラクタでは外部I/Oをしない
    設定読み込み、DB接続、ネットワークアクセスは表示後に寄せる。

  • 設計時判定が必要なら、判定方法を揃える
    Control.DesignMode はタイミングによってfalseになり得るため、判定として弱い。
    実務では LicenseManager.UsageMode を使う方が安定しやすい。

using System.ComponentModel;

private static bool IsDesignTime()
{
    return LicenseManager.UsageMode == LicenseUsageMode.Designtime;
}
  • 設計時に触れてよいのはUI内部の軽い初期値だけ
    画面の形(サイズ/色/初期テキスト)に限る。動的な解決は表示後へ寄せる。

判例(OK/NG)

観点 OK例 NG例 理由(困ること) レビューで見る所
重い初期化の場所 Shownで開始 コンストラクタで開始 デザイナで落ちる/表示が遅れる コンストラクタの処理量
終了の段取り FormClosingで停止要求、FormClosed/Disposeで回収 FormClosingで回収まで全部やる 閉じる操作が重い/順序が読めない FormClosingの中身
タイマー StartとStopを対にする Startだけで放置 Close後も動き続ける Stop/購読解除があるか
イベント購読 購読解除をDisposeに寄せる 購読解除が無い GCされず残りやすい += の対応する -=
非同期 キャンセル経路を持つ 画面寿命と無関係に走らせる Close後のUI更新/ログ増加 CancellationTokenの有無
設計時安全 外部I/Oは表示後 コンストラクタでI/O デザイナが落ちる 設計時判定/処理場所

レビュー観点

観点 ありがちな見落とし 困り方 指摘コメント例(直球禁止)
コンストラクタ肥大 初期化が全部集まる デザイナで落ちる/表示が遅れる 「設計時にも通り得るので、外部I/OはShown以降へ寄せると安定する」
破棄漏れ Timer/購読/Taskの停止が無い Close後も動く/プロセス残留 「開始したものの停止位置が見えない。FormClosedかDisposeへ寄せると追いやすい」
FormClosingの過積載 破棄と待ち合わせが混ざる 閉じる操作が重い 「閉じる判断と回収を分けると順序が読みやすい」
DesignMode頼み 判定が不安定 デザイナだけ落ちる 「DesignModeはタイミングで揺れるので、UsageModeで揃える方が安全」
componentsの扱い Disposeを書き換えるがcomponentsを忘れる 部品が残る/リーク 「Disposeでcomponentsの破棄が維持されているか確認したい」

禁書庫A: 逆引き(症状→原因→対策)

症状 ありがちな原因 切り分け(見る場所) 最短の対処 再発防止(ルール化)
閉じたのにプロセスが残る Timer/購読/バックグラウンドが生きている FormClosed/Disposeに停止処理があるか Stop/購読解除/Cancelを追加 開始と停止を対にし、停止位置をFormClosedかDisposeへ寄せる
設計時に落ちる コンストラクタで外部I/O/DI解決 コンストラクタ/フィールド初期化 重い処理をShownへ移動 コンストラクタは軽量にする。設計時判定が必要ならUsageModeで揃える
Close後に例外ログが増える 非同期がUI更新を続ける Shown開始処理と終了処理 Cancelと終了ガードを入れる 画面寿命に合わせてCancellationTokenを持つ
メモリが戻らない イベント購読解除が無い += 箇所とDispose -= をDisposeに追加 購読解除をDisposeへ寄せ、レビューで対を確認する
閉じる操作が重い FormClosingで待ち合わせ/破棄 FormClosingの中身 停止要求だけにする 停止要求と回収を分け、回収はFormClosed/Disposeへ寄せる

禁書庫B: チートシート(決め打ちで読む)

  • コンストラクタ: InitializeComponentと軽い初期値だけ
  • Load: 軽いUI初期状態(重いI/Oは置かない)
  • Shown: 重い初期化を開始する場所
  • FormClosing: キャンセル判断と停止要求
  • FormClosed: Timer停止、購読解除、終了合図
  • Dispose: 回収の集約点(components破棄を維持する)
.NET Framework 4.8: 併記(差分が有意な箇所だけ)

WinFormsのライフサイクルの考え方自体は同じ。
差分として出やすいのは「設計時判定」と「非同期開始の位置」になる。

  • 設計時判定は LicenseManager.UsageMode を使うと揺れにくい。
  • asyncを使う場合でも、終了合図(CancellationToken)を持たせるのが基本。

関連トピック

1
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
1
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?