C#のDisposeとは?IDisposable・using・GCとの違いと正しい使い方を初心者向けに解説
はじめに
C#でファイル操作、データベース接続、Stream、HttpClient、画像処理などを扱っていると、Dispose、IDisposable、using、GCといった言葉が出てきます。
初心者が特に混乱しやすいのは、「C#にはGCがあるのに、なぜDisposeが必要なのか」という点です。GCは不要になったオブジェクトのメモリを自動的に回収してくれますが、ファイルハンドルやDB接続、ネットワーク接続などのリソースを、開発者が望むタイミングですぐに解放してくれるわけではありません。
そこで使うのがDisposeです。c# disposeを正しく理解すると、リソースリークや「ファイルが使用中で削除できない」「DB接続が枯渇する」「Dispose済みオブジェクトにアクセスして例外が出る」といったトラブルを避けやすくなります。
この記事では、C#のDisposeとは何か、IDisposableやusingとの関係、GCとの違い、正しい使い方、自作クラスでの実装方法、非同期処理で使うDisposeAsyncまで、初心者向けに順番に解説します。
1. C#のDisposeとは?初心者が最初に押さえるべき役割
1-1. Disposeは「不要になったリソースを明示的に解放する」ための仕組み
C#のDisposeとは、使い終わったリソースを明示的に解放するためのメソッドです。
たとえば、ファイルを開く、データベースに接続する、ネットワーク通信を行うといった処理では、C#のオブジェクトだけでなく、OSや外部システム側のリソースも使用します。これらは使い終わったら閉じる必要があります。
Disposeは、そうしたリソースに対して「もう使い終わったので片付けてください」と伝えるための仕組みです。
C#var stream = new FileStream("sample.txt", FileMode.Open);
// ファイルを使う処理
stream.Dispose();
この例では、FileStreamで開いたファイルに関連するリソースを、最後にDisposeで解放しています。
1-2. Disposeはメモリ解放ではなくリソース解放を目的とする
Disposeを理解するうえで重要なのは、Disposeは基本的に「メモリ解放」そのものを目的とした仕組みではないという点です。
C#のメモリ管理はGC、つまりガベージコレクションが担当します。一方、Disposeが主に担当するのは、ファイル、DB接続、ネットワーク接続、OSハンドルなどのリソース解放です。
つまり、次のように役割が分かれます。
| 仕組み | 主な役割 |
|---|---|
| Dispose | ファイル、DB接続、Streamなどのリソースを明示的に解放する |
| GC | 不要になったマネージドオブジェクトのメモリを自動的に回収する |
Disposeを呼んだからといって、そのオブジェクトのメモリがその場ですぐに回収されるとは限りません。メモリの回収タイミングはGCが判断します。
1-3. ファイル・DB接続・StreamなどDisposeが必要になる代表例
Disposeが必要になる代表的なものには、次のようなクラスがあります。
| 種類 | 代表例 |
|---|---|
| ファイル操作 | FileStream, StreamReader, StreamWriter |
| DB接続 | SqlConnection, SqlCommand, SqlDataReader |
| ネットワーク | HttpClient, NetworkStream |
| 画像・描画 | Bitmap, Graphics |
| タイマー・イベント関連 | Timer, CancellationTokenRegistration |
| 暗号化・圧縮 | CryptoStream, GZipStream |
これらの多くはIDisposableを実装しています。IDisposableを実装しているクラスは、使い終わったらDisposeする必要があると考えるのが基本です。
1-4. Disposeを呼ばないと起こる問題
Disposeを呼ばないと、リソースが想定より長く保持される可能性があります。
たとえば、ファイルを開いたままにすると、別の処理でそのファイルを削除・上書きできないことがあります。DB接続を閉じ忘れると、接続数が増え続け、最終的に接続プールが枯渇する可能性があります。
よくある問題は次のとおりです。
| Disposeし忘れ | 起こり得る問題 |
|---|---|
| ファイルを閉じ忘れる | ファイルがロックされたままになる |
| DB接続を閉じ忘れる | 接続数が増え、接続できなくなる |
| Streamを閉じ忘れる | バッファが書き込まれない、リソースが残る |
| イベント購読を解除しない | メモリリークの原因になる |
| OSリソースを解放しない | ハンドルリークの原因になる |
C#ではGCがあるため、最終的には回収される可能性があります。しかし、「いつ回収されるか」は開発者が直接制御できません。そのため、使い終わったタイミングで確実に解放したいリソースにはDisposeが必要です。
2. C#でDisposeが必要になる理由
2-1. C#にはGCがあるのにDisposeが必要な理由
C#にはGCがあるため、不要になったオブジェクトのメモリは自動で回収されます。Microsoftのドキュメントでも、GCはマネージドオブジェクトが使用していたメモリを回収する仕組みであり、IDisposableやIAsyncDisposableはリソースの明示的なクリーンアップに使われると説明されています。
では、なぜDisposeが必要なのでしょうか。
理由は、GCが回収するのは主に「メモリ」であり、ファイルハンドル、DB接続、ソケット、OSハンドルなどを開発者が望むタイミングで即座に解放する仕組みではないからです。
たとえば、ファイルを開いたあとにGCがいつ動くかは分かりません。GCがしばらく動かなければ、ファイルは開かれたままになる可能性があります。
そのため、リソースを使い終わった時点で確実に片付けるためにDisposeを使います。
2-2. マネージドリソースとアンマネージドリソースの違い
C#のリソースを理解するには、マネージドリソースとアンマネージドリソースの違いを押さえる必要があります。
マネージドリソースとは、.NETランタイムが管理しているオブジェクトです。たとえば、通常のC#クラス、配列、文字列、リストなどはマネージドリソースです。
一方、アンマネージドリソースとは、.NETランタイムの管理外にあるリソースです。たとえば、OSのファイルハンドル、ウィンドウハンドル、ネイティブメモリ、ソケット、データベース接続などが該当します。
| 種類 | 説明 | 例 |
|---|---|---|
| マネージドリソース | .NETが管理するオブジェクト | string, List<T>, 通常のクラス |
| アンマネージドリソース | OSや外部環境が管理するリソース | ファイルハンドル、DB接続、ソケット |
Disposeは、特にアンマネージドリソースや、それを内部で保持しているマネージドオブジェクトを適切に解放するために使われます。
2-3. GCが自動で回収できるもの・できないもの
GCが得意なのは、不要になったマネージドオブジェクトのメモリ回収です。
たとえば、次のようなオブジェクトは、参照されなくなればGCの対象になります。
C#var list = new List<string>();
list.Add("A");
list.Add("B");
// listが不要になり、どこからも参照されなくなればGCの対象になる
しかし、ファイルやDB接続のような外部リソースは、単純なメモリとは性質が異なります。
FileStreamオブジェクト自体はマネージドオブジェクトですが、その内部ではOSのファイルハンドルを使っています。このような場合、オブジェクトのメモリだけでなく、内部で保持している外部リソースも解放する必要があります。
そのため、FileStreamのようなクラスにはDisposeが用意されています。
2-4. Disposeは「いつ解放するか」を開発者が制御するために使う
Disposeの大きな価値は、「いつ解放するか」を開発者が明示できることです。
GCは自動で動作しますが、開発者が「今すぐGCを実行して、このリソースを確実に片付けてほしい」と細かく制御するものではありません。
一方、Disposeは次のように使えます。
C#using var reader = new StreamReader("sample.txt");
var text = reader.ReadToEnd();
// スコープを抜けるタイミングでDisposeされる
このコードでは、readerが不要になるスコープの終わりで確実にDisposeが呼ばれます。
つまり、DisposeはGCの代わりではなく、GCでは制御しにくいリソース解放タイミングを開発者が明確にするための仕組みです。
3. IDisposableとは?Disposeとの関係
3-1. IDisposableインターフェースの基本
IDisposableは、Disposeメソッドを持つことを表すインターフェースです。
定義は非常にシンプルです。
C#public interface IDisposable
{
void Dispose();
}
つまり、あるクラスがIDisposableを実装している場合、そのクラスはDisposeメソッドを持っています。
C#public class Sample : IDisposable
{
public void Dispose()
{
// リソース解放処理
}
}
C#では、IDisposableを実装しているオブジェクトは「使い終わったら破棄処理が必要なオブジェクト」と考えるのが基本です。
3-2. IDisposableを実装するとDisposeメソッドを持てる
自作クラスでIDisposableを実装すると、そのクラスにDisposeメソッドを定義する必要があります。
C#public class MyResource : IDisposable
{
public void Dispose()
{
Console.WriteLine("リソースを解放しました");
}
}
このクラスは次のように使えます。
C#using var resource = new MyResource();
// resourceを使う処理
usingを使うことで、スコープの終わりにDisposeが自動的に呼び出されます。
3-3. IDisposableを実装している主なクラス
C#や.NETには、IDisposableを実装しているクラスが多数あります。
代表的なものは次のとおりです。
C#FileStream
StreamReader
StreamWriter
MemoryStream
SqlConnection
SqlCommand
SqlDataReader
HttpClient
Timer
Bitmap
Graphics
CancellationTokenSource
ただし、MemoryStreamのように実際にはアンマネージドリソースを直接持たないケースでも、基底クラスや設計上の理由でIDisposableを実装しているものがあります。
重要なのは、内部実装を推測するよりも、「型がIDisposableを実装しているなら、基本的にはDispose対象」と考えることです。
3-4. IDisposableを見つけたらDisposeが必要と判断する
初心者がDisposeの必要性を判断する最も簡単な方法は、型がIDisposableを実装しているかを見ることです。
Visual StudioやRiderなどのIDEでは、型にマウスを合わせたり、定義へ移動したりすると、IDisposableを実装しているか確認できます。
また、usingに渡せるかどうかも判断材料になります。
C#using var reader = new StreamReader("sample.txt");
このようにusingで使える型は、基本的にIDisposableまたはIAsyncDisposableを実装しています。
4. using文とは?Disposeを安全に呼ぶ書き方
4-1. using文を使うとDisposeを自動で呼び出せる
using文は、Disposeを自動的に呼び出すための構文です。
C#using (var reader = new StreamReader("sample.txt"))
{
var text = reader.ReadToEnd();
Console.WriteLine(text);
}
このコードでは、usingブロックを抜けるタイミングでreader.Dispose()が自動的に呼ばれます。
自分で次のように書かなくてもよくなります。
C#var reader = new StreamReader("sample.txt");
try
{
var text = reader.ReadToEnd();
Console.WriteLine(text);
}
finally
{
reader.Dispose();
}
MicrosoftのC#リファレンスでも、using文はブロック内で例外が発生しても、対象の破棄可能インスタンスを確実に破棄する構文として説明されています。
4-2. using文はtry-finallyに展開される
using文は、内部的にはtry-finallyに近い形で動作します。
たとえば、次のコードがあります。
C#using (var reader = new StreamReader("sample.txt"))
{
var text = reader.ReadToEnd();
}
これは概念的には次のような処理です。
C#var reader = new StreamReader("sample.txt");
try
{
var text = reader.ReadToEnd();
}
finally
{
if (reader != null)
{
reader.Dispose();
}
}
finallyは、通常終了でも例外発生でも実行されます。そのため、usingを使うと、例外時でもDisposeが呼ばれやすくなります。
4-3. usingステートメントとusing宣言の違い
C#には、従来のusingステートメントと、C# 8.0以降で使えるusing宣言があります。
従来のusingステートメントは、ブロックでスコープを明示します。
C#using (var reader = new StreamReader("sample.txt"))
{
var text = reader.ReadToEnd();
}
一方、using宣言はブロックを書かず、変数が宣言されたスコープの終わりでDisposeされます。
C#using var reader = new StreamReader("sample.txt");
var text = reader.ReadToEnd();
// このスコープを抜けるとDisposeされる
違いは、Disposeされるタイミングです。
| 書き方 | Disposeされるタイミング |
|---|---|
using (...) { } | usingブロックを抜けたとき |
using var | 変数が宣言されたスコープを抜けたとき |
短い処理ではusingステートメント、メソッド全体で使いたい場合はusing宣言が便利です。
4-4. 例外が発生してもDisposeされる理由
usingを使うと、ブロック内で例外が発生してもDisposeが呼ばれます。
C#using (var reader = new StreamReader("sample.txt"))
{
throw new Exception("エラーが発生しました");
}
この場合でも、usingブロックを抜ける際にDisposeが実行されます。
これは、usingがtry-finally相当の構文だからです。finallyは例外が発生しても実行されるため、リソース解放処理に向いています。
4-5. usingを使うべき場面と使わないほうがよい場面
usingを使うべき場面は、オブジェクトを作成した側が、そのオブジェクトの所有者であり、使い終わったらすぐ解放してよい場合です。
C#using var connection = new SqlConnection(connectionString);
connection.Open();
// DB処理
一方、使わないほうがよい場面もあります。
たとえば、DIコンテナから取得したオブジェクトを勝手にusingで囲むのは避けるべきです。
C#// DIから注入されたdbContextを勝手にDisposeするのは避ける
public class UserService
{
private readonly AppDbContext _dbContext;
public UserService(AppDbContext dbContext)
{
_dbContext = dbContext;
}
}
DIコンテナがライフタイムを管理しているオブジェクトは、基本的にDIコンテナに破棄を任せます。自分で作ったものは自分でDisposeし、借りたものは勝手にDisposeしない、という考え方が重要です。
5. Dispose・IDisposable・using・GCの違いを整理
5-1. Disposeはリソース解放処理そのもの
Disposeは、リソースを解放するための処理そのものです。
C#stream.Dispose();
このように直接呼び出すこともできます。
ただし、手動でDisposeを書くと、例外が発生した場合に呼び忘れるリスクがあります。そのため、通常はusingを使って自動的にDisposeされるようにするのが推奨されます。
5-2. IDisposableはDisposeを持つことを示すインターフェース
IDisposableは、「このクラスはDisposeできます」という約束を表すインターフェースです。
C#public class MyClass : IDisposable
{
public void Dispose()
{
// 破棄処理
}
}
IDisposableを実装している型は、使い終わったあとにDisposeを呼ぶ必要がある可能性が高いです。
5-3. usingはDisposeを自動実行する構文
usingは、IDisposableを実装したオブジェクトのDisposeを自動で呼び出す構文です。
C#using var reader = new StreamReader("sample.txt");
このように書くことで、明示的にDisposeを書かなくても、スコープ終了時に自動で破棄されます。
つまり、usingはDisposeを安全に呼ぶための便利な書き方です。
5-4. GCは不要なメモリを回収する仕組み
GCは、不要になったマネージドオブジェクトのメモリを自動的に回収する仕組みです。
ただし、GCは開発者が望むタイミングでリソースを即座に解放するための仕組みではありません。
C#var data = new byte[1024 * 1024];
// dataが不要になればGCの対象になる
このようなメモリはGCが管理します。
一方、ファイルやDB接続などは、使い終わった時点でDisposeするのが基本です。
5-5. DisposeとGCの役割を比較表で理解する
DisposeとGCの違いを整理すると、次のようになります。
| 項目 | Dispose | GC |
|---|---|---|
| 主な目的 | リソースの明示的な解放 | メモリの自動回収 |
| 対象 | ファイル、DB接続、Stream、OSリソースなど | 不要になったマネージドオブジェクト |
| 実行タイミング | 開発者が制御できる | ランタイムが判断する |
| 呼び出し方法 | Dispose(), using | 自動 |
| 即時性 | 使い終わった時点で解放できる | いつ実行されるかは不定 |
| 代表例 | FileStream.Dispose() | 参照されなくなったオブジェクトの回収 |
初心者は、「Disposeはリソースを閉じるもの、GCはメモリを片付けるもの」と考えると理解しやすいです。
6. Disposeの基本的な使い方
6-1. StreamReaderを使ったDisposeの基本例
まずはStreamReaderを使った基本例です。
C#var reader = new StreamReader("sample.txt");
try
{
string text = reader.ReadToEnd();
Console.WriteLine(text);
}
finally
{
reader.Dispose();
}
このコードでは、finallyでDisposeを呼んでいます。
ただし、実際のC#では、次のようにusingを使うほうが一般的です。
C#using var reader = new StreamReader("sample.txt");
string text = reader.ReadToEnd();
Console.WriteLine(text);
このほうが短く、安全で、読みやすいコードになります。
6-2. SqlConnectionを使ったDisposeの基本例
データベース接続でもDisposeは重要です。
C#using var connection = new SqlConnection(connectionString);
connection.Open();
using var command = new SqlCommand("SELECT COUNT(*) FROM Users", connection);
int count = (int)command.ExecuteScalar();
Console.WriteLine(count);
SqlConnectionやSqlCommandはIDisposableを実装しています。そのため、使い終わったらDisposeする必要があります。
usingを使うことで、DB接続やコマンドを安全に解放できます。
6-3. Disposeを手動で呼び出す書き方
Disposeは手動で呼び出すこともできます。
C#var stream = new FileStream("sample.txt", FileMode.Open);
stream.Dispose();
ただし、この書き方はおすすめできない場面が多いです。
理由は、Disposeを呼ぶ前に例外が発生すると、Disposeが実行されない可能性があるからです。
C#var stream = new FileStream("sample.txt", FileMode.Open);
DoSomething(stream); // ここで例外が発生するとDisposeされない
stream.Dispose();
この問題を避けるため、通常はusingを使います。
6-4. usingを使った推奨の書き方
推奨される書き方は次のような形です。
C#using var stream = new FileStream("sample.txt", FileMode.Open);
DoSomething(stream);
または、スコープを明確にしたい場合は次のように書きます。
C#using (var stream = new FileStream("sample.txt", FileMode.Open))
{
DoSomething(stream);
}
処理が短い場合や、解放タイミングを明確にしたい場合はusingステートメントが分かりやすいです。
メソッド全体で使う場合はusing宣言が便利です。
6-5. Dispose後のオブジェクトを使ってはいけない理由
Disposeしたオブジェクトは、基本的にもう使ってはいけません。
C#var reader = new StreamReader("sample.txt");
reader.Dispose();
var text = reader.ReadToEnd(); // NG
このようなコードでは、ObjectDisposedExceptionが発生する可能性があります。
Disposeは「このオブジェクトが保持しているリソースを解放する」という意味です。リソースを解放したあとに同じオブジェクトを使うと、内部状態が無効になっているため、正しく動作しません。
Dispose後に再度使いたい場合は、新しいインスタンスを作成します。
C#using var reader = new StreamReader("sample.txt");
var text = reader.ReadToEnd();
7. IDisposableを自作クラスに実装する方法
7-1. 自作クラスでIDisposableが必要になるケース
自作クラスでIDisposableが必要になるのは、主に次のようなケースです。
| ケース | 例 |
|---|---|
クラス内でIDisposableなオブジェクトを保持している | StreamReader, SqlConnectionをフィールドに持つ |
| アンマネージドリソースを直接扱っている | IntPtrやネイティブハンドルを扱う |
| イベント購読やタイマーを明示的に解除したい | イベント解除、Timer.Dispose() |
| 長く保持すると問題になる外部リソースを使う | ファイル、DB、ネットワーク |
たとえば、クラス内にStreamReaderを保持している場合、そのクラスもIDisposableを実装して、内部のStreamReaderを破棄できるようにします。
7-2. 最小限のIDisposable実装例
最小限の実装は次のようになります。
C#public class FileLoader : IDisposable
{
private readonly StreamReader _reader;
public FileLoader(string path)
{
_reader = new StreamReader(path);
}
public string ReadAll()
{
return _reader.ReadToEnd();
}
public void Dispose()
{
_reader.Dispose();
}
}
使う側は次のように書けます。
C#using var loader = new FileLoader("sample.txt");
string text = loader.ReadAll();
Console.WriteLine(text);
FileLoaderが内部でStreamReaderを所有しているため、FileLoader.Dispose()の中で_reader.Dispose()を呼びます。
7-3. Dispose内で保持しているIDisposableオブジェクトを解放する
自作クラスが複数のIDisposableオブジェクトを保持している場合、それらをDispose内で解放します。
C#public class ReportWriter : IDisposable
{
private readonly FileStream _fileStream;
private readonly StreamWriter _writer;
public ReportWriter(string path)
{
_fileStream = new FileStream(path, FileMode.Create);
_writer = new StreamWriter(_fileStream);
}
public void Write(string text)
{
_writer.WriteLine(text);
}
public void Dispose()
{
_writer.Dispose();
_fileStream.Dispose();
}
}
ただし、この例ではStreamWriterをDisposeすると、内部のFileStreamも閉じられる場合があります。実際の設計では、どのオブジェクトがどのリソースを所有しているかを意識する必要があります。
基本は「自分が作成し、所有しているものをDisposeする」です。
7-4. 二重Disposeを防ぐためのフラグ管理
Disposeは複数回呼ばれる可能性があります。
C#resource.Dispose();
resource.Dispose();
Disposeは複数回呼ばれても問題が起きないように実装するのが望ましいです。そのため、フラグで二重Disposeを防ぎます。
C#public class MyResource : IDisposable
{
private bool _disposed;
private readonly StreamReader _reader;
public MyResource(string path)
{
_reader = new StreamReader(path);
}
public void Dispose()
{
if (_disposed)
{
return;
}
_reader.Dispose();
_disposed = true;
}
}
このようにしておくと、Disposeが複数回呼ばれても、実際の解放処理は一度だけ実行されます。
7-5. Dispose後に使われた場合の例外設計
Dispose後にメソッドが呼ばれた場合は、ObjectDisposedExceptionを投げる設計にすると分かりやすいです。
C#public class MyResource : IDisposable
{
private bool _disposed;
private readonly StreamReader _reader;
public MyResource(string path)
{
_reader = new StreamReader(path);
}
public string ReadAll()
{
ThrowIfDisposed();
return _reader.ReadToEnd();
}
public void Dispose()
{
if (_disposed)
{
return;
}
_reader.Dispose();
_disposed = true;
}
private void ThrowIfDisposed()
{
if (_disposed)
{
throw new ObjectDisposedException(nameof(MyResource));
}
}
}
これにより、Dispose後に誤って使われた場合でも、原因が分かりやすくなります。
8. Disposeパターンとは?正しい実装の考え方
8-1. Disposeパターンが必要になるケース
Disposeパターンとは、IDisposableを正しく実装するための定型的な書き方です。
特に次のような場合に重要です。
| ケース | Disposeパターンの必要性 |
|---|---|
| アンマネージドリソースを直接持つ | 必要性が高い |
| ファイナライザーを持つ | 必要 |
| 継承される可能性がある | protected virtual Dispose(bool)を検討 |
IDisposableなフィールドを持つだけ | 簡易実装で十分な場合も多い |
Microsoftの実装ガイドでは、アンマネージドリソースを扱う場合、Dispose(bool)を使ったパターンや、可能であればSafeHandleでラップする方法が説明されています。
初心者の場合、まずは「自作クラスがIDisposableなフィールドを持つなら、そのフィールドをDisposeする」と考えれば十分です。アンマネージドリソースを直接扱うようになったら、Disposeパターンを理解しましょう。
8-2. Dispose(bool disposing)の役割
Disposeパターンでは、次のようなメソッドを使います。
C#protected virtual void Dispose(bool disposing)
{
}
disposingは、通常のDisposeから呼ばれたのか、ファイナライザーから呼ばれたのかを区別するための引数です。
| disposing | 意味 |
|---|---|
true | Dispose()から呼ばれた。マネージドリソースも解放してよい |
false | ファイナライザーから呼ばれた。アンマネージドリソースだけを解放する |
ファイナライザーから呼ばれた場合、他のマネージドオブジェクトがすでに回収済み、または回収途中である可能性があります。そのため、disposing == falseのときはマネージドリソースに触らないようにします。
8-3. GC.SuppressFinalizeを呼ぶ理由
GC.SuppressFinalize(this)は、「このオブジェクトのファイナライザーはもう実行しなくてよい」とGCに伝えるために使います。
C#public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}
すでにDispose()でリソース解放が完了しているなら、後からファイナライザーを実行する必要はありません。
ファイナライザーはGCにとって負荷が高く、オブジェクトの回収タイミングにも影響します。そのため、Dispose()で片付けが済んだ場合はGC.SuppressFinalize(this)を呼ぶのが一般的です。
ただし、ファイナライザーを実装していないクラスでは、必ずしもGC.SuppressFinalize(this)が必要とは限りません。Disposeパターンとして書く場合に登場するものと理解しましょう。
8-4. ファイナライザーとDisposeの違い
ファイナライザーは、オブジェクトがGCに回収される前に呼ばれる可能性がある特別なメソッドです。
C#~MyResource()
{
Dispose(false);
}
Disposeとの違いは次のとおりです。
| 項目 | Dispose | ファイナライザー |
|---|---|---|
| 呼び出す人 | 開発者、using | GC |
| 実行タイミング | 明示的に制御できる | 不定 |
| 主な用途 | 使い終わった時点でリソース解放 | Disposeし忘れ時の保険 |
| マネージドリソースの解放 | 可能 | 原則避ける |
| 実装頻度 | 比較的多い | 必要な場合のみ |
ファイナライザーはDisposeの代わりではありません。あくまで保険です。
8-5. 継承を考慮したDisposeパターンの実装例
継承を考慮した基本的なDisposeパターンは次のようになります。
C#public class BaseResource : IDisposable
{
private bool _disposed;
private StreamReader? _reader;
public BaseResource(string path)
{
_reader = new StreamReader(path);
}
public void Read()
{
ThrowIfDisposed();
string text = _reader!.ReadToEnd();
Console.WriteLine(text);
}
public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}
protected virtual void Dispose(bool disposing)
{
if (_disposed)
{
return;
}
if (disposing)
{
_reader?.Dispose();
_reader = null;
}
_disposed = true;
}
protected void ThrowIfDisposed()
{
if (_disposed)
{
throw new ObjectDisposedException(nameof(BaseResource));
}
}
}
継承先で追加のリソースを持つ場合は、Dispose(bool disposing)をオーバーライドします。
C#public class DerivedResource : BaseResource
{
private bool _disposed;
private StreamWriter? _writer;
public DerivedResource(string readPath, string writePath)
: base(readPath)
{
_writer = new StreamWriter(writePath);
}
protected override void Dispose(bool disposing)
{
if (_disposed)
{
return;
}
if (disposing)
{
_writer?.Dispose();
_writer = null;
}
_disposed = true;
base.Dispose(disposing);
}
}
ポイントは、派生クラスのリソースを解放したあと、最後にbase.Dispose(disposing)を呼ぶことです。
9. Disposeでよくある誤解と間違い
9-1. GCがあるからDisposeは不要という誤解
「C#にはGCがあるからDisposeは不要」と考えるのは誤解です。
GCはメモリ管理を自動化してくれますが、外部リソースを適切なタイミングで解放する責任まで完全に肩代わりしてくれるわけではありません。
ファイル、DB接続、ソケットなどは、使い終わったらDisposeで明示的に解放する必要があります。
正しくは、次のように理解しましょう。
| 誤解 | 正しい理解 |
|---|---|
| GCがあるからDispose不要 | GCは主にメモリ回収、Disposeはリソース解放 |
| GCがいつか片付けるから問題ない | いつ片付くか分からないため、リソース不足になる可能性がある |
| Disposeは古い書き方 | 現在のC#でも重要な仕組み |
9-2. Disposeを呼べばメモリがすぐ解放されるという誤解
Disposeを呼ぶと、関連リソースは解放されますが、そのオブジェクトのメモリがその場ですぐに回収されるとは限りません。
C#stream.Dispose();
このコードは、streamが持つファイルリソースなどを解放します。しかし、streamオブジェクト自体のメモリ回収はGCのタイミング次第です。
そのため、Disposeは「メモリを即座に消す命令」ではなく、「使い終わったリソースを解放する命令」と理解しましょう。
9-3. finalizerがあればDisposeしなくてよいという誤解
ファイナライザーがあるクラスでも、Disposeしなくてよいわけではありません。
ファイナライザーはGCによって呼ばれるため、実行タイミングが不定です。アプリケーション終了直前まで呼ばれない可能性もあります。
また、ファイナライザーはGCの負担を増やすため、通常のリソース解放手段として使うべきではありません。
ファイナライザーはあくまで「Disposeし忘れた場合の最後の保険」です。
9-4. IDisposableを実装していないクラスにDisposeは不要
すべてのクラスにDisposeが必要なわけではありません。
たとえば、次のような通常のデータクラスにはDisposeは不要です。
C#public class User
{
public int Id { get; set; }
public string Name { get; set; } = "";
}
このクラスはファイルやDB接続などを保持していません。単なるデータを表すクラスなので、IDisposableを実装する必要はありません。
Disposeが必要かどうかは、「解放すべきリソースを持っているか」「IDisposableなオブジェクトを所有しているか」で判断します。
9-5. Disposeを何度も呼んでしまう問題
Disposeが複数回呼ばれること自体は珍しくありません。
そのため、Disposeの実装では、複数回呼ばれても安全に動くようにしておくべきです。
C#public void Dispose()
{
if (_disposed)
{
return;
}
// 解放処理
_disposed = true;
}
使う側でも、Dispose後のオブジェクトを再利用しないように注意が必要です。
C#resource.Dispose();
// ここから先でresourceを使わない
10. Disposeが必要か判断するチェックポイント
10-1. 型がIDisposableを実装しているか確認する
最初に確認すべきポイントは、対象の型がIDisposableを実装しているかどうかです。
C#public class SomeClass : IDisposable
{
public void Dispose()
{
}
}
IDisposableを実装している場合、その型はリソース解放が必要な可能性があります。
IDEで型定義を確認したり、公式ドキュメントを見たりして、IDisposableの実装有無を確認しましょう。
10-2. ファイル・ネットワーク・DB・OSリソースを扱っているか確認する
次に確認するのは、そのクラスが外部リソースを扱っているかどうかです。
| 扱うもの | Disposeが必要になりやすい理由 |
|---|---|
| ファイル | ファイルハンドルを使うため |
| DB接続 | 接続数に上限があるため |
| ネットワーク | ソケットなどを使うため |
| OSハンドル | .NET管理外のリソースを使うため |
| タイマー | コールバックや内部リソースを持つため |
これらを扱うクラスは、Disposeが必要になる可能性が高いです。
10-3. 所有しているリソースか借りているリソースかを判断する
Disposeすべきかどうかは、そのリソースを「所有しているか」「借りているだけか」でも変わります。
自分でnewしたオブジェクトは、基本的に自分が所有者です。
C#using var stream = new FileStream("sample.txt", FileMode.Open);
この場合は、自分で作ったstreamなので、自分でDisposeします。
一方、外部から渡されたオブジェクトは、自分が所有者とは限りません。
C#public void Write(Stream stream)
{
// streamを使うが、ここでDisposeしてよいとは限らない
}
このような場合、勝手にstream.Dispose()すると、呼び出し元がまだ使う予定だったリソースを閉じてしまう可能性があります。
原則として、自分で作ったものは自分でDisposeし、外部から借りたものは勝手にDisposeしないようにします。
10-4. DIコンテナを使う場合のDisposeの扱い
ASP.NET CoreなどでDIコンテナを使っている場合、DIコンテナが生成したオブジェクトの破棄は、基本的にDIコンテナに任せます。
C#public class UserService
{
private readonly AppDbContext _dbContext;
public UserService(AppDbContext dbContext)
{
_dbContext = dbContext;
}
}
この例では、AppDbContextをコンストラクタで受け取っています。このような場合、UserService内で_dbContext.Dispose()を呼ぶべきではありません。
DIコンテナがスコープの終了時などに適切に破棄します。
ただし、自分で一時的に作成したオブジェクトについては、自分でusingする必要があります。
10-5. ライブラリ利用時にドキュメントを確認する
ライブラリを使う場合は、公式ドキュメントやAPIリファレンスでDisposeの扱いを確認しましょう。
特に次のような点を見ると判断しやすいです。
| 確認項目 | 見るポイント |
|---|---|
IDisposableを実装しているか | 実装していればDispose候補 |
| サンプルコード | usingで囲まれているか |
| ライフタイム管理 | 呼び出し側が破棄する必要があるか |
| DI対応 | コンテナが破棄する前提か |
| Closeメソッドの有無 | Disposeとの関係が説明されているか |
C#では、ライブラリごとにリソースの所有関係が異なることがあります。迷ったらドキュメントを確認するのが安全です。
11. 非同期処理で使うDisposeAsyncとawait using
11-1. DisposeAsyncとは何か
DisposeAsyncは、非同期でリソースを解放するためのメソッドです。
同期的なDisposeではなく、非同期処理を伴うクリーンアップが必要な場合に使います。
C#await resource.DisposeAsync();
たとえば、ネットワーク越しに終了処理を行う、非同期でバッファをフラッシュする、非同期APIを使ってリソースを閉じる、といった場面で使われます。
Microsoftのドキュメントでは、IAsyncDisposable.DisposeAsync()は非同期の破棄操作を表すValueTaskを返すと説明されています。
11-2. IAsyncDisposableとIDisposableの違い
IDisposableとIAsyncDisposableの違いは、破棄処理が同期か非同期かです。
| インターフェース | メソッド | 用途 |
|---|---|---|
IDisposable | void Dispose() | 同期的なリソース解放 |
IAsyncDisposable | ValueTask DisposeAsync() | 非同期のリソース解放 |
IDisposableは従来からある同期的な破棄の仕組みです。
IAsyncDisposableは、非同期処理が一般化したC#で、非同期にリソース解放を行うために使われます。
11-3. await usingの基本的な使い方
IAsyncDisposableを実装したオブジェクトは、await usingで扱えます。
C#await using var resource = new AsyncResource();
await resource.DoSomethingAsync();
await usingを使うと、スコープ終了時にDisposeAsyncが呼ばれ、その完了をawaitします。
従来のusingがDisposeを呼ぶのに対して、await usingはDisposeAsyncを呼ぶと考えると分かりやすいです。
11-4. 非同期でリソース解放が必要になるケース
非同期でリソース解放が必要になる代表例は次のとおりです。
| ケース | 理由 |
|---|---|
| 非同期ストリーム | 終了時に非同期フラッシュが必要な場合がある |
| ネットワーク通信 | 切断処理が非同期になる場合がある |
| DB関連処理 | 非同期APIで接続を閉じる場合がある |
| クラウドストレージ | 終了処理が通信を伴う場合がある |
| 非同期バッファ処理 | 書き込み完了を待つ必要がある |
同期的に閉じるとスレッドをブロックしてしまう処理では、DisposeAsyncが有効です。
11-5. DisposeとDisposeAsyncの使い分け
DisposeとDisposeAsyncは、対象の型に応じて使い分けます。
| 型が実装しているもの | 使う構文 |
|---|---|
IDisposable | using |
IAsyncDisposable | await using |
| 両方 | 利用シーンとドキュメントに従う |
通常のファイル操作や単純なリソース解放ではDisposeで十分な場合が多いです。
一方、非同期APIを使うクラスや、解放処理そのものが非同期になるクラスではDisposeAsyncを使います。
C#await using var resource = new AsyncResource();
await resource.ExecuteAsync();
非同期メソッド内では、await usingを使うことで自然にリソース解放できます。
12. C#のDisposeに関するよくある質問
12-1. Disposeは必ず呼ぶべき?
IDisposableを実装しているオブジェクトは、基本的に使い終わったらDisposeすべきです。
ただし、DIコンテナが管理しているオブジェクトや、外部から借りているオブジェクトなど、自分が所有していないものは勝手にDisposeしないほうがよい場合があります。
判断の基本は次のとおりです。
| 状況 | Disposeする? |
|---|---|
自分でnewした | 基本的にする |
usingで作成した | 自動でされる |
| DIから受け取った | 基本的にDIに任せる |
| 引数で受け取った | 所有者でないなら勝手にしない |
IDisposableでない | 通常は不要 |
12-2. usingを書けばDisposeは不要?
usingを書いている場合、自分でDispose()を直接呼ぶ必要はありません。
C#using var reader = new StreamReader("sample.txt");
// 自分でreader.Dispose()を書く必要はない
usingがスコープ終了時にDisposeを呼んでくれるためです。
むしろ、using内で途中で手動Disposeすると、その後にオブジェクトを使ってしまうリスクがあります。
C#using var reader = new StreamReader("sample.txt");
reader.Dispose();
var text = reader.ReadToEnd(); // NG
基本的には、usingに任せるのが安全です。
12-3. DisposeとCloseの違いは?
Closeは「閉じる」という意味のメソッドで、ファイルや接続などに用意されていることがあります。
一方、DisposeはIDisposableインターフェースで定義された標準的な破棄メソッドです。
クラスによっては、Closeが内部的にDisposeを呼ぶ、またはDisposeと同等の処理をする場合があります。
C#reader.Close();
reader.Dispose();
ただし、C#ではusingと組み合わせやすいDisposeが標準的なリソース解放の仕組みです。
迷った場合は、usingを使うのが基本です。
C#using var reader = new StreamReader("sample.txt");
12-4. Disposeし忘れはどうやって見つける?
Disposeし忘れを見つける方法はいくつかあります。
まず、IDEの警告やコード分析を活用します。Visual StudioやRiderでは、IDisposableオブジェクトの破棄漏れを警告してくれる場合があります。
また、次のような症状がある場合はDisposeし忘れを疑います。
| 症状 | 可能性 |
|---|---|
| ファイルが削除できない | ファイルを閉じ忘れている |
| DB接続エラーが増える | 接続を解放していない |
| メモリ使用量が増え続ける | リソースやイベント購読が残っている |
ObjectDisposedExceptionが出る | Dispose後のオブジェクトを使っている |
| ハンドル数が増え続ける | OSリソースを解放していない |
コードレビューでは、newしているIDisposable型がusingで囲まれているかを確認するとよいです。
12-5. Disposeはnullチェックしてから呼ぶべき?
手動でDisposeを呼ぶ場合、変数がnullの可能性があるならnullチェックが必要です。
C#if (resource != null)
{
resource.Dispose();
}
C#ではnull条件演算子を使って簡潔に書けます。
C#resource?.Dispose();
ただし、usingを使う場合は、通常自分でnullチェックを書く必要はありません。
C#using var reader = new StreamReader("sample.txt");
自作クラスのDispose内では、フィールドがnullになる可能性を考慮して、次のように書くことがあります。
C#_reader?.Dispose();
_reader = null;
まとめ
C#のDisposeは、使い終わったリソースを明示的に解放するための重要な仕組みです。
初心者がまず押さえるべきポイントは、Disposeはメモリ解放そのものではなく、ファイル、DB接続、Stream、ネットワーク接続、OSリソースなどを適切なタイミングで解放するために使うということです。
IDisposableは、Disposeメソッドを持つことを示すインターフェースです。型がIDisposableを実装している場合、そのオブジェクトは使い終わったらDisposeする必要がある可能性が高いです。
usingは、Disposeを安全に自動実行するための構文です。例外が発生してもスコープ終了時にDisposeされるため、C#でリソースを扱うときは、基本的にusingまたはusing varを使うのが推奨されます。
一方、GCは不要になったマネージドオブジェクトのメモリを回収する仕組みです。GCがあるからといって、Disposeが不要になるわけではありません。
最後に、判断の基本を整理します。
| 判断ポイント | 対応 |
|---|---|
型がIDisposableを実装している | 基本的にDispose対象 |
自分でnewした | 自分でDisposeする |
| DIから受け取った | DIコンテナに任せる |
| 外部から借りた | 勝手にDisposeしない |
| 非同期破棄が必要 | DisposeAsyncとawait usingを使う |
C#でDisposeを正しく使えるようになると、リソースリークを防ぎ、安定したアプリケーションを作りやすくなります。まずは「IDisposableを見つけたらusingを検討する」という習慣から始めるとよいでしょう。

