連載Index(読む順・公開済リンクが最新): S00_門前の誓い_総合Index
IDisposable を見つけた時に止まりやすいのは、どこで Dispose を呼ぶかです。
そのメソッドの中で閉じるのか、クラスの Dispose で閉じるのか、外から渡されたのでここでは閉じないのか。
ここが曖昧なままだと、閉じ忘れか、閉じなくていいものまで閉じるかのどちらかになりやすくなります。
このページでは、レビューで先に止める 5 つのルールを並べます。
日常コードなら、まずこの 5 つで切り分ければ十分な場面がかなり多いです。
- メソッド内で終わるもの
- フィールドで持つもの
- 外から渡されたもの
- 呼び出し側へ返すもの
- OS 資源を直接持つもの
最初にルールを置き、そのあとに短い例を並べます。
.NET 8 を前提にしていますが、考え方は .NET Framework 4.8.1 でもほぼ同じです。
最初に見るのは型の名前ではなく寿命です。
そのメソッドで終わるのか、クラスで持ち続けるのか、外から渡されたのかで先に分けると、Dispose を書く位置が見えやすくなります。
1. 先に固定する 5 つのルール
先に固定してよいルールは、次の 5 つです。
- メソッド内で完結する
IDisposableはusingに入れます。 - フィールドで保持する
IDisposableは、そのクラスがDisposeを持ちます。 - DI で受け取った
IDisposableは、そのクラスでは閉じません。 -
IDisposableを返すメソッドは、受け取った側に閉じる・解除する責務があると名前から読めるようにします。 - P/Invoke 境界では、
IntPtrをそのまま返さず、SafeHandleを優先します。
細かい例外はあります。
ただ、日常コードでは先にこの 5 つを固定した方が、閉じ忘れと二重解放の両方を減らしやすくなります。
このページは、例外を全部並べるより、普段のレビューで止める基準をそろえることを優先しています。
まずは 5 つを固定し、例外はそのあとで扱う方が崩れにくくなります。
2. 先に判断表
| 場面 |
Dispose を呼ぶ側 |
書き方 | 典型例 |
|---|---|---|---|
| メソッド内で終わる | そのメソッド | using var |
FileStream, StreamReader, StreamWriter
|
| フィールドで持つ | そのクラス |
IDisposable 実装 |
FileSystemWatcher, CancellationTokenSource
|
| 外から渡された | 渡してきた側 | ここでは閉じない | DI で受け取る HttpClient
|
| 呼び出し側へ返す | 呼び出し側 | 名前で責務が読めるように返す |
FileStream を返すメソッド、CancellationToken.Register()
|
| OS 資源を直接持つ |
SafeHandle を持つ側 |
SafeHandle を使う |
Win32 ハンドル |
先にこの表のどこへ入るかを決めます。
そこが決まると、Dispose をどこで呼ぶかも決めやすくなります。
3. メソッド内で終わるものは using に入れます
開く、使う、終わる。
この 3 つが 1 つのメソッドで終わるなら、原則として using に入れます。
public static string LoadText(string path)
{
using var stream = File.OpenRead(path);
using var reader = new StreamReader(stream);
return reader.ReadToEnd();
}
public static void SaveText(string path, string text)
{
using var stream = File.Create(path);
using var writer = new StreamWriter(stream);
writer.Write(text);
}
この書き方なら、return で抜けても、例外で抜けても閉じます。
イベント内で完結する処理も同じです。
using を先に考えるのは次のような場面です。
- ファイルを開く
- 読み込む
- 書き込む
- そのメソッドを抜けたあとに使わない
開く、使う、終わるが同じメソッドで完結するのに using を使っていない形は、先に見直した方が安全です。
4. フィールドで持つものは、そのクラスで閉じます
クラスのメンバーとして持ち続けるなら、そのクラスが終わらせます。
using ではなく、クラス側に Dispose を書きます。
public sealed class WatchService : IDisposable
{
private readonly FileSystemWatcher _watcher;
private readonly CancellationTokenSource _cts = new();
private bool _disposed;
public WatchService(string path)
{
_watcher = new FileSystemWatcher(path);
_watcher.EnableRaisingEvents = true;
}
public void Dispose()
{
if (_disposed) return;
_watcher.Dispose();
_cts.Dispose();
_disposed = true;
}
}
ここで確認するのは 2 つです。
- フィールドで持っているか
- 閉じる処理が同じクラスの中にあるか
作る処理だけあって、閉じる処理が見当たらない形は、あとで追いにくくなります。
フィールドへ置いた IDisposable は、そのクラスの Dispose までセットで書きます。
作る処理だけあって閉じる処理がない形は、そのまま残しません。
次のようなメンバーは残りやすいです。
FileSystemWatcherTimerCancellationTokenSourceStreamWriterBitmap
5. 外から渡されたものは、ここでは閉じません
DI で受け取ったものは、このクラスで作っていません。
その場合は、このクラスでは閉じません。
public sealed class ApiWorker
{
private readonly HttpClient _client;
public ApiWorker(HttpClient client)
{
_client = client;
}
public Task<string> GetAsync(string url)
{
return _client.GetStringAsync(url);
}
}
このクラスは HttpClient を使うだけで、作ってはいません。
ここで Dispose まで持つと、別の所で使っている HttpClient まで終わることがあります。
外から渡されたものを確認する時は、まず new した所を見ます。
ここで作っていないなら、閉じる側ではないことが多いです。
DI で受け取った IDisposable は、このクラスでは閉じません。
ここで閉じるのは、そのクラスが生成責務まで持つ場合だけに絞った方が安全です。
6. 呼び出し側へ返すなら、受け取った側で閉じます
返したあとに誰が閉じるか読めないと、呼び出し側で using を書き忘れやすくなります。
この形では、受け取った側が閉じます。
public static FileStream OpenLogStream(string path)
{
return new FileStream(path, FileMode.Append, FileAccess.Write, FileShare.Read);
}
使う側はこうなります。
using var stream = OpenLogStream(path);
using var writer = new StreamWriter(stream);
writer.WriteLine(message);
コメントで CancellationToken.Register() のような例もあると指摘をもらったため、この節を見直しました。
IDisposable を返すメソッド名は Open / Create に限らず、受け取った側に閉じる・解除する責務があると名前から読めることを基準にしています。
ここで大事なのは、IDisposable を返していることが名前から読めることです。
Open や Create は分かりやすい例ですが、それだけに限りません。
たとえば CancellationToken.Register() のように、登録解除の責務が戻り値にある形もあります。
IDisposable を返すメソッド名は、受け取った側に閉じる・解除する責務があると読み取りやすい方が分かりやすくなります。
Open、Create、Register など、戻り値の意味が分かる名前を使います。
7. Finalize が出てくる場面
Finalize が必要になる場面はかなり少なめです。
Stream や HttpClient を持つくらいなら、普通は Dispose だけで足ります。
Finalize が話に出るのは、だいたい次のような時です。
- 直接アンマネージ資源を持っている
- 継承をまたいで後始末を書く
-
Dispose(bool disposing)を使う必要がある
ここで押さえるのは 1 つだけです。
Finalize を普段の解放手段にしないことです。
いつ走るかに頼る形だと、閉じる時期が読みにくくなります。
普段の業務コードで Finalize を書き始めたら、いったん立ち止まった方が安全です。
本当に必要なのか、Dispose と SafeHandle で足りないのかを先に見た方が手戻りが減ります。
8. SafeHandle を使います
OS 側のハンドルを直接持つ時は、IntPtr のまま回すより SafeHandle を優先します。
後始末をどこで持つかが見えやすくなるからです。
using Microsoft.Win32.SafeHandles;
using System.Runtime.InteropServices;
internal static class NativeMethods
{
[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 OpenReadHandle(string path)
{
const uint GENERIC_READ = 0x80000000;
const uint FILE_SHARE_READ = 0x00000001;
const uint OPEN_EXISTING = 3;
var handle = CreateFile(
path,
GENERIC_READ,
FILE_SHARE_READ,
IntPtr.Zero,
OPEN_EXISTING,
0,
IntPtr.Zero);
if (handle.IsInvalid)
{
throw new InvalidOperationException("CreateFile failed.");
}
return handle;
}
}
使う側はこうなります。
using var handle = NativeMethods.OpenReadHandle(path);
ここで確認するのは、IntPtr をそのまま外へ出していないかどうかです。
その形だと、後始末をどこで持つのかが分かれやすく、追いにくくなります。
P/Invoke 境界では、IntPtr をそのまま返さない方が安全です。
返り値として外へ出す必要があるなら、まず SafeHandle にできないかを見ます。
9. 確認表
| 観点 | 確認場所 | レビューで止める項目 | 確認内容 |
|---|---|---|---|
using |
メソッド内のローカル変数 | 開くだけで閉じる行がない | その場で終わるなら using
|
| フィールド保持 | クラスのメンバー |
new はあるが閉じる処理がない |
クラスで Dispose を持つ |
| DI | コンストラクタ引数 | 受け取ったものまで閉じる | ここで作っていないなら閉じない |
| 返り値 | メソッド名と戻り値 | 返したあと閉じる側が読めない | 受け取った側が閉じる責務を読めるか |
| Win32 |
IntPtr / SafeHandle
|
IntPtr をそのまま回す |
SafeHandle を使う |
10. 手が止まった時の順番
- そのメソッドだけで終わるか
- フィールドで持つか
- ここで
newしたか - 呼び出し側へ返すか
- OS 資源を直接持つか
ここまで見れば、どこで Dispose を呼ぶかはほぼ決まります。
IDisposable を見つけるたびに最初から考え直すより、この順で見た方が早く整理できます。
この順番で見る理由は、前から順に日常コードで出る回数が多いからです。
using とフィールド保持だけで止まることもかなり多いです。
11. レビューで止める項目
DI で受け取ったものを、その場の using に入れています
そのクラスで作っていないものまで閉じています。
生成責務と利用責務が混ざっていないかを見直します。
フィールドで持っているのに Dispose がありません
FileSystemWatcher、Timer、CancellationTokenSource はここで残りやすいです。
作る処理と閉じる処理が同じクラスにあるかを確認します。
IDisposable を返しているのに名前から責務が読めません
OpenXxx() や CreateXxx() は分かりやすい例です。
それに限らず、受け取った側に閉じる・解除する責務があることを名前から読み取れるかを見ます。
IntPtr をそのまま回しています
この形は、後始末をどこで持つかが見えにくくなります。
SafeHandle にできないかを先に見ます。
関連リンク
連載Index(読む順・公開済リンクが最新): S00_門前の誓い_総合Index