C#のIDisposableとは?Dispose・usingの使い方から正しい実装タイミングまで初心者向けに解説

はじめに

C#でファイル操作、データベース接続、ネットワーク通信などを扱っていると、IDisposableDisposeusing という言葉をよく見かけます。

初心者のうちは、

IDisposableって何のためにあるの?」
Dispose()はいつ呼べばいいの?」
usingを書いておけば本当に大丈夫?」
「自作クラスにもIDisposableを実装すべき?」

と迷うことが多いはずです。

結論からいうと、IDisposable使い終わったリソースを明示的に解放するための仕組みです。特に、ファイル、データベース接続、ネットワーク接続、OSのハンドル、画像リソースなど、プログラムの外側にある資源を扱う場合に重要になります。

C#にはガベージコレクションがあるため、不要になったオブジェクトのメモリは自動的に回収されます。しかし、ガベージコレクションだけではファイルや接続などのリソースを適切なタイミングで解放できないことがあります。そこで使われるのがIDisposableDisposeです。

この記事では、C#のIDisposableについて、基本的な考え方からDisposeの使い方、using文・using宣言、正しい実装方法、実装すべきタイミングまで初心者向けにわかりやすく解説します。

1. C#のIDisposableとは?まず押さえるべき基本

1-1. IDisposableは「不要になったリソースを解放する」ための仕組み

IDisposableは、C#でリソースを明示的に解放するためのインターフェースです。

定義は非常にシンプルで、次のようになっています。

C#
public interface IDisposable
{
void Dispose();
}

IDisposableを実装したクラスは、Dispose()メソッドを持ちます。このDispose()を呼び出すことで、そのオブジェクトが使用していたリソースを解放できます。

たとえば、ファイルを開く、データベースに接続する、ネットワーク通信を開始する、といった処理では、プログラムの外部にあるリソースを使用します。これらは使い終わったら閉じる必要があります。

IDisposableは、その「使い終わったら片付ける」という処理を統一的に扱うための仕組みです。

1-2. Disposeメソッドの役割とは

Disposeメソッドの役割は、オブジェクトが保持しているリソースを解放することです。

たとえば、次のようなリソースが対象になります。

C#
FileStream stream = new FileStream("sample.txt", FileMode.Open);
stream.Dispose();

この例では、FileStreamが開いているファイルをDispose()で閉じています。

Dispose()を呼ぶことで、ファイルに対するロックが解除されたり、OSが管理しているハンドルが解放されたりします。つまり、Dispose()は単に「C#のオブジェクトを消す」ものではなく、そのオブジェクトが内部で使っている外部リソースを片付けるためのメソッドです。

1-3. メモリ解放とリソース解放の違い

IDisposableを理解するうえで重要なのが、メモリ解放リソース解放の違いです。

C#では、通常のオブジェクトのメモリ管理はガベージコレクションが行います。たとえば、使われなくなったクラスのインスタンスは、将来的にGCによって自動的に回収されます。

一方で、ファイル、データベース接続、ソケット、ウィンドウハンドルなどは、単なる.NETのメモリではありません。これらはOSや外部システムが管理しているリソースです。

つまり、次のように考えるとわかりやすいです。

メモリ解放     :C#のオブジェクトが使っていたメモリを回収すること
リソース解放 :ファイル、接続、ハンドルなど外部資源を閉じること

Dispose()が主に担当するのは、後者のリソース解放です。

1-4. ガベージコレクションだけでは不十分な理由

C#にはガベージコレクションがあるため、「放っておけばいつか解放されるのでは?」と思うかもしれません。

しかし、ガベージコレクションがいつ実行されるかは基本的に開発者が制御できません。不要になったオブジェクトがすぐに回収されるとは限らないのです。

たとえば、ファイルを開いたままDispose()しないと、しばらくファイルがロックされた状態になることがあります。その結果、別の処理で同じファイルを開こうとしたときにエラーになる可能性があります。

データベース接続でも同じです。接続を適切に閉じないと、接続プールを圧迫し、アプリケーション全体のパフォーマンス低下や接続エラーにつながることがあります。

このように、ガベージコレクションだけに任せると、リソース解放のタイミングが遅くなり、問題が発生することがあります。そのため、IDisposableを実装しているオブジェクトは、使い終わったタイミングで明示的にDispose()することが大切です。

2. IDisposableが必要になる代表的なケース

2-1. ファイル・ストリームを扱う場合

IDisposableが必要になる代表例が、ファイルやストリームを扱う場合です。

たとえば、FileStreamStreamReaderStreamWriterなどはIDisposableを実装しています。

C#
using var stream = new FileStream("sample.txt", FileMode.Open);

ファイルを開くと、OS側ではファイルハンドルが確保されます。これを解放しないままにすると、ファイルがロックされたままになったり、他の処理がファイルにアクセスできなくなったりします。

ファイル操作では、使い終わったら必ず閉じる必要があります。そのため、FileStreamなどはDispose()によってファイルハンドルを解放する仕組みになっています。

2-2. データベース接続を扱う場合

データベース接続もIDisposableが重要な場面です。

たとえば、SQL Serverに接続するSqlConnectionIDisposableを実装しています。

C#
using var connection = new SqlConnection(connectionString);
connection.Open();

データベース接続は、開いたままにするとサーバーや接続プールのリソースを消費します。接続を閉じ忘れると、接続数の上限に達して新しい接続ができなくなることもあります。

SqlConnectionDispose()を呼ぶことで接続を閉じ、関連するリソースを解放します。実際の開発では、SqlConnectionはほとんどの場合usingと組み合わせて使います。

2-3. ネットワーク通信やソケットを扱う場合

ネットワーク通信やソケットを扱う場合も、IDisposableが必要です。

たとえば、TcpClientUdpClientSocketHttpClientなどが関係します。

C#
using var client = new TcpClient();

ネットワーク接続は、OSのソケットや通信バッファなどを使用します。これらを適切に解放しないと、接続が残ったり、ポートやソケットリソースを無駄に消費したりします。

ただし、HttpClientについては少し注意が必要です。HttpClientIDisposableを実装していますが、短時間に何度も生成して破棄するのではなく、アプリケーション全体で再利用する設計が推奨されることがあります。

つまり、IDisposableだからといって、常に短いスコープでusingすればよいわけではありません。対象のクラスの性質に応じて、適切なライフタイムを考える必要があります。

2-4. 画像・ハンドル・アンマネージリソースを扱う場合

画像、フォント、ブラシ、ウィンドウハンドルなどを扱う場合にも、IDisposableが関係します。

たとえば、BitmapGraphicsなどは内部でGDI+のリソースを使用します。

C#
using var bitmap = new Bitmap("image.png");
using var graphics = Graphics.FromImage(bitmap);

これらのリソースは.NETのメモリだけではなく、OS側の資源を消費します。適切に解放しないと、メモリリークのような状態になったり、画像処理が失敗したりすることがあります。

特に画像処理や帳票出力、Windowsアプリケーションなどでは、IDisposableの扱いが重要になります。

2-5. IDisposableを実装したクラスをフィールドに持つ場合

自作クラスがIDisposableを実装したオブジェクトをフィールドとして保持する場合、その自作クラスもIDisposableを実装すべきか検討する必要があります。

たとえば、次のようなクラスです。

C#
public class ReportWriter
{
private readonly StreamWriter _writer;

public ReportWriter(string path)
{
_writer = new StreamWriter(path);
}
}

このクラスは内部でStreamWriterを保持しています。StreamWriterIDisposableなので、ReportWriter側でも適切なタイミングで_writer.Dispose()を呼ぶ必要があります。

そのため、次のようにReportWriterIDisposableを実装するのが自然です。

C#
public class ReportWriter : IDisposable
{
private readonly StreamWriter _writer;

public ReportWriter(string path)
{
_writer = new StreamWriter(path);
}

public void Dispose()
{
_writer.Dispose();
}
}

このように、直接アンマネージリソースを扱っていなくても、IDisposableなメンバーを所有している場合は、自分自身もIDisposableになることがあります。

3. Disposeの基本的な使い方

3-1. 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);

stream.Dispose();

DoSomething(stream)の中で例外が発生すると、stream.Dispose()は実行されません。これではファイルが開いたままになる可能性があります。

そのため、実際のコードでは手動でDispose()を呼ぶよりも、usingtry-finallyを使うのが基本です。

3-2. Disposeを呼び忘れると起きる問題

Dispose()を呼び忘れると、さまざまな問題が起きる可能性があります。

たとえば、ファイル操作では、ファイルがロックされたままになり、別の処理から書き込みや削除ができなくなることがあります。

データベース接続では、接続が閉じられず、接続プールを圧迫する可能性があります。大量のリクエストを処理するWebアプリケーションでは、接続の解放漏れが障害につながることもあります。

画像やハンドルを扱う処理では、OSのリソースを消費し続け、アプリケーションの動作が不安定になることがあります。

つまり、Dispose()の呼び忘れは、単なるコード上のミスではなく、実行時の不具合やパフォーマンス低下につながる重要な問題です。

3-3. try-finallyで確実にDisposeする方法

Dispose()を確実に呼び出すための基本的な書き方がtry-finallyです。

C#
FileStream? stream = null;

try
{
stream = new FileStream("sample.txt", FileMode.Open);
// streamを使った処理
}
finally
{
stream?.Dispose();
}

finallyブロックは、tryブロック内で例外が発生しても実行されます。そのため、リソース解放処理を書く場所として適しています。

ただし、毎回このようにtry-finallyを書くのは面倒です。そこでC#では、using文やusing宣言を使って、より簡潔に安全なリソース解放を書けるようになっています。

実際、using文は内部的にはtry-finallyに近い形で展開されます。

3-4. Dispose後のオブジェクトを使ってはいけない理由

Dispose()を呼んだ後のオブジェクトは、基本的に再利用してはいけません。

C#
var stream = new FileStream("sample.txt", FileMode.Open);
stream.Dispose();

stream.ReadByte(); // 不適切

Dispose()後は、内部のリソースがすでに解放されています。その状態でメソッドを呼び出すと、ObjectDisposedExceptionが発生することがあります。

Dispose()は「一時停止」ではなく、「もう使い終わった」という意味です。

そのため、Dispose()後に再度使いたい場合は、同じオブジェクトを再利用するのではなく、新しくインスタンスを作成するのが基本です。

4. using文・using宣言でIDisposableを安全に扱う

4-1. using文とは?Disposeを自動で呼び出す構文

using文は、IDisposableを実装したオブジェクトのDispose()を自動で呼び出すための構文です。

次のように書きます。

C#
using (var stream = new FileStream("sample.txt", FileMode.Open))
{
// streamを使った処理
}

この場合、usingブロックを抜けるタイミングで、自動的にstream.Dispose()が呼び出されます。

例外が発生した場合でも、Dispose()は呼び出されます。そのため、ファイルやデータベース接続などを扱うときは、基本的にusingを使うのが安全です。

4-2. using文の基本構文とサンプルコード

using文の基本構文は次のとおりです。

C#
using (リソースの作成)
{
// リソースを使う処理
}

ファイルを読み込む例を見てみましょう。

C#
using (var reader = new StreamReader("sample.txt"))
{
string text = reader.ReadToEnd();
Console.WriteLine(text);
}

このコードでは、StreamReaderを使ってファイルを読み込んでいます。usingブロックを抜けると、自動的にreader.Dispose()が呼ばれます。

つまり、次のような処理をC#が自動的に行ってくれます。

C#
StreamReader? reader = null;

try
{
reader = new StreamReader("sample.txt");
string text = reader.ReadToEnd();
Console.WriteLine(text);
}
finally
{
reader?.Dispose();
}

using文を使うことで、コードが短くなり、Dispose()の呼び忘れも防げます。

4-3. C# 8.0以降のusing宣言の書き方

C# 8.0以降では、using宣言という書き方も使えます。

C#
using var reader = new StreamReader("sample.txt");

string text = reader.ReadToEnd();
Console.WriteLine(text);

using宣言では、波かっこでブロックを作りません。変数が宣言されたスコープを抜けるタイミングで、自動的にDispose()が呼ばれます。

たとえば、メソッド内でusing varを書いた場合、そのメソッドを抜けるときにDispose()されます。

C#
public void ReadFile()
{
using var reader = new StreamReader("sample.txt");

string text = reader.ReadToEnd();
Console.WriteLine(text);

} // ここでreader.Dispose()が呼ばれる

using宣言はコードをすっきり書けるため、最近のC#ではよく使われます。

4-4. using文とusing宣言の違い

using文とusing宣言の大きな違いは、Dispose()されるタイミングです。

using文では、usingブロックを抜けた時点でDispose()されます。

C#
using (var reader = new StreamReader("sample.txt"))
{
string text = reader.ReadToEnd();
}

// ここではreaderはすでにDispose済み

一方、using宣言では、宣言されたスコープを抜けるタイミングでDispose()されます。

C#
public void Sample()
{
using var reader = new StreamReader("sample.txt");

string text = reader.ReadToEnd();

// この時点ではまだreaderはDisposeされていない
} // ここでDisposeされる

つまり、早めにリソースを解放したい場合はusing文が向いています。メソッド全体で使い、最後に解放されればよい場合はusing宣言が便利です。

4-5. usingを使うべき場面と注意点

IDisposableを実装しているオブジェクトを短いスコープで使う場合は、基本的にusingを使うべきです。

代表的な例は次のようなものです。

C#
using var stream = new FileStream("sample.txt", FileMode.Open);
using var reader = new StreamReader(stream);

ただし、注意点もあります。

usingを使うと、スコープを抜けた時点でDispose()されます。そのため、usingの外側でそのオブジェクトを使おうとすると問題になります。

C#
StreamReader reader;

using (reader = new StreamReader("sample.txt"))
{
Console.WriteLine(reader.ReadLine());
}

// ここでreaderを使うのは不適切

また、オブジェクトのライフタイムが長い場合や、アプリケーション全体で再利用すべきオブジェクトに対して、安易に短いusingを使うのは避けたほうがよいこともあります。

usingは便利ですが、「どの範囲でそのリソースを使うのか」を意識して使うことが大切です。

5. IDisposableの正しい実装方法

5-1. IDisposableを実装する基本コード

自作クラスでIDisposableを実装する基本形は次のようになります。

C#
public class MyResource : IDisposable
{
private bool _disposed;

public void Dispose()
{
if (_disposed)
{
return;
}

// リソース解放処理を書く

_disposed = true;
}
}

この例では、_disposedというフラグを使って、すでにDispose()済みかどうかを管理しています。

Dispose()は複数回呼ばれても問題が起きないように作るのが基本です。なぜなら、呼び出し元が誤って複数回Dispose()してしまう可能性があるからです。

5-2. Disposeパターンとは

C#では、IDisposableを実装する際によく使われる定型的な書き方があります。これをDisposeパターンと呼びます。

特に、継承される可能性があるクラスや、アンマネージリソースを直接扱うクラスでは、次のような形が使われます。

C#
public class MyResource : IDisposable
{
private bool _disposed;

public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}

protected virtual void Dispose(bool disposing)
{
if (_disposed)
{
return;
}

if (disposing)
{
// マネージリソースを解放する
}

// アンマネージリソースを解放する

_disposed = true;
}
}

このパターンでは、Dispose()からDispose(true)を呼び出します。Dispose(bool disposing)の中で、マネージリソースとアンマネージリソースを分けて解放します。

初心者のうちは少し難しく見えるかもしれませんが、重要なのは「リソース解放処理を安全にまとめるための定型パターン」だと理解することです。

5-3. protected virtual Dispose(bool disposing)の意味

protected virtual Dispose(bool disposing)には、主に2つの意味があります。

1つ目は、継承先のクラスで解放処理を追加できるようにすることです。

C#
protected virtual void Dispose(bool disposing)
{
// 基底クラスの解放処理
}

virtualになっているため、派生クラスでoverrideできます。

C#
protected override void Dispose(bool disposing)
{
if (disposing)
{
// 派生クラスのマネージリソースを解放
}

base.Dispose(disposing);
}

2つ目は、disposing引数によって、通常のDispose()から呼ばれたのか、ファイナライザーから呼ばれたのかを区別することです。

disposingtrueの場合は、明示的にDispose()が呼ばれたという意味です。この場合、他のマネージオブジェクトを安全に解放できます。

disposingfalseの場合は、ファイナライザーから呼ばれたケースを想定します。この場合、他のマネージオブジェクトの状態は保証できないため、基本的にはアンマネージリソースだけを解放します。

5-4. GC.SuppressFinalizeを呼ぶ理由

GC.SuppressFinalize(this)は、ファイナライザーの呼び出しを抑制するためのメソッドです。

C#
public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}

ファイナライザーとは、オブジェクトがガベージコレクションされる前に呼ばれる可能性がある後始末用の仕組みです。

ただし、すでにDispose()でリソースを解放済みであれば、ファイナライザーを実行する必要はありません。そこで、GC.SuppressFinalize(this)を呼んで「このオブジェクトのファイナライザーはもう呼ばなくてよい」とGCに伝えます。

ファイナライザーがあるクラスでは、Dispose()内でGC.SuppressFinalize(this)を呼ぶのが基本です。

なお、ファイナライザーを定義していないクラスでは、GC.SuppressFinalize(this)が必須というわけではありません。ただし、Disposeパターンの定型として書かれることがあります。

5-5. ファイナライザーが必要なケース・不要なケース

ファイナライザーは、アンマネージリソースを直接扱う場合に必要になることがあります。

たとえば、ネイティブAPIを呼び出して取得したハンドルやポインタを自分で管理している場合です。

C#
public class NativeResource : IDisposable
{
private bool _disposed;

~NativeResource()
{
Dispose(false);
}

public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}

protected virtual void Dispose(bool disposing)
{
if (_disposed)
{
return;
}

if (disposing)
{
// マネージリソースの解放
}

// アンマネージリソースの解放

_disposed = true;
}
}

一方で、通常のアプリケーション開発では、ファイナライザーが必要になるケースは多くありません。

たとえば、FileStreamSqlConnectionなど、すでにIDisposableを実装している.NETのクラスをフィールドに持つだけなら、自分でファイナライザーを書く必要はほとんどありません。

むやみにファイナライザーを書くと、ガベージコレクションの負荷が増え、オブジェクトの回収が遅くなる可能性があります。

初心者のうちは、アンマネージリソースを直接扱っていない限り、ファイナライザーは基本的に不要と考えてよいでしょう。

5-6. 多重Disposeに対応する実装の考え方

Dispose()は、複数回呼ばれても安全に動作するように実装するのが一般的です。

たとえば、次のように_disposedフラグを使います。

C#
public class SampleResource : IDisposable
{
private bool _disposed;
private StreamWriter? _writer;

public SampleResource(string path)
{
_writer = new StreamWriter(path);
}

public void Dispose()
{
if (_disposed)
{
return;
}

_writer?.Dispose();
_writer = null;

_disposed = true;
}
}

この実装では、1回目のDispose()_writerを解放し、2回目以降は何もしません。

多重Disposeに対応していないと、すでに解放済みのリソースを再度解放しようとして例外や不具合が発生する可能性があります。

安全なIDisposable実装では、「何度呼ばれても問題ない」ことを意識しましょう。

6. IDisposableを実装すべきタイミング・不要なタイミング

6-1. 自作クラスでIDisposableを実装すべき判断基準

自作クラスにIDisposableを実装すべきかどうかは、次の基準で考えるとわかりやすいです。

そのクラスが、使い終わったタイミングで明示的に解放すべきリソースを所有しているなら、IDisposableを実装するべきです。

たとえば、次のような場合です。

C#
public class FileLogger : IDisposable
{
private readonly StreamWriter _writer;

public FileLogger(string path)
{
_writer = new StreamWriter(path);
}

public void Write(string message)
{
_writer.WriteLine(message);
}

public void Dispose()
{
_writer.Dispose();
}
}

このFileLoggerは内部でStreamWriterを所有しています。StreamWriterは解放が必要なので、FileLoggerIDisposableを実装するのが適切です。

6-2. IDisposableなメンバーを持つクラスはDisposeが必要か

IDisposableなメンバーを持つクラスは、基本的に自分もIDisposableを実装することを検討します。

ただし、重要なのは「所有しているかどうか」です。

たとえば、コンストラクタの中で自分が作成したStreamWriterをフィールドとして保持しているなら、そのクラスが責任を持って破棄すべきです。

C#
public class ReportService : IDisposable
{
private readonly StreamWriter _writer;

public ReportService()
{
_writer = new StreamWriter("report.txt");
}

public void Dispose()
{
_writer.Dispose();
}
}

一方で、外部から渡されたオブジェクトを一時的に使うだけで、その所有権が呼び出し元にある場合は、自分のDispose()で破棄してよいか慎重に判断する必要があります。

C#
public class ReportService
{
private readonly TextWriter _writer;

public ReportService(TextWriter writer)
{
_writer = writer;
}
}

この場合、writerを破棄する責任がReportServiceにあるとは限りません。呼び出し元が引き続き使う可能性があるからです。

IDisposableなメンバーを持つ場合は、「誰が作ったのか」「誰が使い終わりを判断するのか」を考えることが大切です。

6-3. マネージリソースだけの場合はどう考えるか

マネージリソースだけを扱っている場合でも、その中にIDisposableなオブジェクトが含まれていれば、Dispose()が必要になることがあります。

たとえば、StreamWriterSqlConnectionは.NETのクラスなのでマネージオブジェクトですが、内部ではファイルハンドルやデータベース接続などを扱っています。

そのため、自作クラスがこれらを所有しているなら、自作クラスもIDisposableを実装するべきです。

一方で、単にstringList<T>Dictionary<TKey, TValue>などを持っているだけなら、通常はIDisposableを実装する必要はありません。

C#
public class UserCache
{
private readonly Dictionary<int, string> _users = new();
}

このようなクラスは、ガベージコレクションに任せれば十分です。

6-4. 迷ったときの実装判断フロー

IDisposableを実装すべきか迷ったときは、次の流れで判断するとよいでしょう。

1. アンマネージリソースを直接扱っているか?
→ はい:IDisposableを実装する

2. IDisposableを実装したオブジェクトを所有しているか?
→ はい:IDisposableを実装する

3. 外部から渡されたIDisposableを一時的に使うだけか?
→ はい:所有権を確認する。勝手にDisposeしない

4. stringやListなど通常のマネージオブジェクトだけか?
→ はい:基本的にIDisposableは不要

ポイントは、「解放すべきリソースを自分が所有しているか」です。

所有しているなら、使い終わったときに解放する責任があります。所有していないなら、勝手にDispose()すると呼び出し元の処理を壊してしまう可能性があります。

6-5. なんでもIDisposableにすればよいわけではない理由

IDisposableは便利ですが、すべてのクラスに実装すればよいわけではありません。

不要なクラスにIDisposableを実装すると、使う側に余計な負担を与えます。

たとえば、次のようなクラスにIDisposableを実装しても意味はほとんどありません。

C#
public class User
{
public string Name { get; set; } = "";
public int Age { get; set; }
}

このクラスは単なるデータを持っているだけです。解放すべき外部リソースはありません。

それにもかかわらずIDisposableを実装すると、利用者は「このクラスは何か解放が必要なのか」と考えることになります。結果として、コードの意図がわかりにくくなります。

IDisposableは、必要なときにだけ使うべき仕組みです。

7. 初心者がつまずきやすいIDisposableの注意点

7-1. Disposeとデストラクタを混同しない

C#では、Dispose()とデストラクタ、正確にはファイナライザーを混同しやすいです。

Dispose()は、開発者が明示的に呼び出すリソース解放メソッドです。

C#
resource.Dispose();

一方、ファイナライザーは、ガベージコレクションのタイミングで呼ばれる可能性がある後始末用の仕組みです。

C#
~MyClass()
{
// ファイナライザー
}

ファイナライザーはいつ呼ばれるかわかりません。そのため、ファイルやデータベース接続を適切なタイミングで解放する目的には向いていません。

基本はDispose()で明示的に解放し、ファイナライザーはアンマネージリソースを直接扱う特殊なケースで使うものと考えましょう。

7-2. Disposeを呼べば必ずメモリがすぐ解放されるわけではない

Dispose()を呼ぶと、メモリがすぐに解放されると思う人もいますが、これは正確ではありません。

Dispose()は主にリソースを解放するためのメソッドです。オブジェクト自体のメモリを回収するのはガベージコレクションの役割です。

たとえば、次のコードでDispose()を呼んでも、streamというオブジェクトのメモリがその瞬間に必ず回収されるわけではありません。

C#
stream.Dispose();

ただし、内部で開いていたファイルハンドルなどは解放されます。

つまり、Dispose()は「メモリを即座に消す」ためのものではなく、「不要になったリソースを明示的に片付ける」ためのものです。

7-3. usingのスコープを間違えない

usingを使うときは、スコープに注意が必要です。

次のコードでは、usingブロックを抜けたあとにreaderを使うことはできません。

C#
using (var reader = new StreamReader("sample.txt"))
{
string line = reader.ReadLine() ?? "";
}

// readerはここでは使えない

usingブロックを抜けると、reader.Dispose()が呼ばれます。つまり、ブロックの外側ではリソースはすでに解放済みです。

using宣言の場合も同様です。宣言されたスコープを抜けるとDisposeされます。

C#
public void Read()
{
using var reader = new StreamReader("sample.txt");

Console.WriteLine(reader.ReadLine());
} // ここでDisposeされる

usingは便利ですが、オブジェクトをどこまで使うのかを意識してスコープを決める必要があります。

7-4. 例外発生時でもDisposeされるようにする

リソースを扱うコードでは、例外が発生してもDispose()されるように書くことが重要です。

手動でDispose()を書くと、例外時に呼び出されない可能性があります。

C#
var reader = new StreamReader("sample.txt");

string text = reader.ReadToEnd();

reader.Dispose();

このコードでは、ReadToEnd()で例外が発生するとDispose()が呼ばれません。

安全に書くなら、usingを使います。

C#
using var reader = new StreamReader("sample.txt");

string text = reader.ReadToEnd();

この書き方なら、例外が発生してもスコープを抜けるときにDispose()が呼ばれます。

7-5. Dispose済みオブジェクトの再利用を避ける

一度Dispose()したオブジェクトは、再利用しないようにしましょう。

C#
using var stream = new FileStream("sample.txt", FileMode.Open);

stream.Dispose();

stream.ReadByte(); // 不適切

Dispose()済みのオブジェクトを使うと、ObjectDisposedExceptionが発生することがあります。

また、クラスを設計する側では、Dispose()後にメソッドが呼ばれた場合に例外を投げるようにすることもあります。

C#
private void ThrowIfDisposed()
{
if (_disposed)
{
throw new ObjectDisposedException(nameof(MyResource));
}
}

このようにして、Dispose後の誤った利用を早期に検出できます。

8. 実践サンプルで理解するIDisposable

8-1. FileStreamをusingで安全に扱う例

FileStreamは、ファイルを読み書きするための代表的なクラスです。IDisposableを実装しているため、使い終わったらDispose()する必要があります。

C#
using var stream = new FileStream("sample.txt", FileMode.OpenOrCreate);

byte[] data = System.Text.Encoding.UTF8.GetBytes("Hello, IDisposable!");
stream.Write(data, 0, data.Length);

このコードでは、using varを使っているため、メソッドのスコープを抜けると自動的にstream.Dispose()が呼ばれます。

より明示的にスコープを限定したい場合は、using文を使います。

C#
using (var stream = new FileStream("sample.txt", FileMode.OpenOrCreate))
{
byte[] data = System.Text.Encoding.UTF8.GetBytes("Hello, IDisposable!");
stream.Write(data, 0, data.Length);
}

この場合、usingブロックを抜けた時点でファイルが閉じられます。

8-2. StreamReaderを使ったファイル読み込みの例

ファイルの内容を読み込む場合は、StreamReaderを使います。

C#
using var reader = new StreamReader("sample.txt");

string text = reader.ReadToEnd();

Console.WriteLine(text);

StreamReaderIDisposableを実装しています。内部でストリームを扱うため、使い終わったら解放が必要です。

1行ずつ読み込む場合は、次のように書けます。

C#
using var reader = new StreamReader("sample.txt");

string? line;
while ((line = reader.ReadLine()) != null)
{
Console.WriteLine(line);
}

usingを使えば、読み込み中に例外が発生しても、最終的にDispose()が呼ばれます。

8-3. SqlConnectionをusingで閉じる例

データベース接続では、SqlConnectionusingで扱うのが基本です。

C#
using var connection = new SqlConnection(connectionString);

connection.Open();

using var command = connection.CreateCommand();
command.CommandText = "SELECT Id, Name FROM Users";

using var reader = command.ExecuteReader();

while (reader.Read())
{
int id = reader.GetInt32(0);
string name = reader.GetString(1);

Console.WriteLine($"{id}: {name}");
}

このコードでは、SqlConnectionSqlCommandSqlDataReaderをそれぞれusing varで扱っています。

処理が終わると、スコープの終了時に逆順でDispose()されます。つまり、readercommandconnectionの順に解放されます。

データベース接続は高価なリソースなので、開いたままにせず、使い終わったら確実に閉じることが重要です。

8-4. 自作クラスにIDisposableを実装する例

次に、自作クラスでIDisposableを実装する例を見てみましょう。

C#
public class CsvWriter : IDisposable
{
private readonly StreamWriter _writer;
private bool _disposed;

public CsvWriter(string path)
{
_writer = new StreamWriter(path);
}

public void WriteRow(params string[] values)
{
ThrowIfDisposed();

string line = string.Join(",", values);
_writer.WriteLine(line);
}

public void Dispose()
{
if (_disposed)
{
return;
}

_writer.Dispose();
_disposed = true;
}

private void ThrowIfDisposed()
{
if (_disposed)
{
throw new ObjectDisposedException(nameof(CsvWriter));
}
}
}

このクラスは内部でStreamWriterを使っています。そのため、CsvWriter自身もIDisposableを実装し、Dispose()内で_writer.Dispose()を呼び出しています。

利用側は次のように書けます。

C#
using var writer = new CsvWriter("users.csv");

writer.WriteRow("Id", "Name");
writer.WriteRow("1", "Alice");
writer.WriteRow("2", "Bob");

このように、自作クラスがIDisposableなオブジェクトを所有している場合は、自作クラスでもDispose()を用意すると、利用者が安全に扱えるようになります。

8-5. Dispose呼び忘れを防ぐ書き方の比較

Dispose()の呼び忘れを防ぐには、手動呼び出しよりもusingを使うのが基本です。

手動で呼ぶ書き方は次のようになります。

C#
var writer = new StreamWriter("sample.txt");

writer.WriteLine("Hello");

writer.Dispose();

一見問題なさそうですが、WriteLineで例外が発生するとDispose()が呼ばれません。

try-finallyを使うと安全です。

C#
StreamWriter? writer = null;

try
{
writer = new StreamWriter("sample.txt");
writer.WriteLine("Hello");
}
finally
{
writer?.Dispose();
}

ただし、少し冗長です。

usingを使えば、次のように簡潔に書けます。

C#
using var writer = new StreamWriter("sample.txt");

writer.WriteLine("Hello");

初心者がまず覚えるべきなのは、IDisposableを実装しているオブジェクトは、基本的にusingで扱うということです。

9. IDisposable・Dispose・usingに関するよくある質問

9-1. Disposeはいつ呼ばれるのか

Dispose()は、自動的にいつでも呼ばれるわけではありません。

基本的には、開発者が明示的に呼び出します。

C#
resource.Dispose();

また、using文やusing宣言を使っている場合は、スコープを抜けるタイミングで自動的に呼ばれます。

C#
using var resource = new MyResource();

この場合、変数resourceが宣言されたスコープを抜けるときにDispose()が呼ばれます。

重要なのは、Dispose()はガベージコレクションと同じではないということです。GCがいつか実行されるとしても、リソースを適切なタイミングで解放したいなら、Dispose()またはusingを使う必要があります。

9-2. usingを使えばDisposeを書かなくてよいのか

利用する側のコードでは、usingを使えば明示的にDispose()を書く必要はありません。

C#
using var reader = new StreamReader("sample.txt");

このコードでは、スコープの終了時に自動的にreader.Dispose()が呼ばれます。

ただし、自作クラスを作る側では、そのクラスが解放すべきリソースを所有しているなら、Dispose()を実装する必要があります。

つまり、利用者側ではusingを使い、クラス設計側では必要に応じてIDisposableを実装する、という役割分担になります。

9-3. DisposeとCloseの違いは何か

C#のクラスには、Dispose()のほかにClose()メソッドを持つものがあります。たとえば、ストリームやデータベース接続などです。

多くの場合、Close()はリソースを閉じるためのメソッドで、内部的にはDispose()と同じような処理を行うことがあります。

C#
connection.Close();
connection.Dispose();

ただし、IDisposableの標準的な解放方法はDispose()です。そして、usingDispose()を自動的に呼び出します。

そのため、C#では基本的にClose()を手動で呼ぶよりも、usingDispose()されるように書くのが一般的です。

C#
using var connection = new SqlConnection(connectionString);
connection.Open();

Close()があるクラスでも、IDisposableを実装しているならusingを使うと安全です。

9-4. IDisposableは必ず実装しなければならないのか

すべてのクラスでIDisposableを実装する必要はありません。

IDisposableが必要なのは、明示的に解放すべきリソースを所有している場合です。

たとえば、次のようなクラスでは通常不要です。

C#
public class Product
{
public int Id { get; set; }
public string Name { get; set; } = "";
}

このクラスは単なるデータを持っているだけで、ファイルや接続などのリソースを扱っていません。

一方で、次のようなクラスではIDisposableを検討します。

C#
public class ProductExporter : IDisposable
{
private readonly StreamWriter _writer;

public ProductExporter(string path)
{
_writer = new StreamWriter(path);
}

public void Dispose()
{
_writer.Dispose();
}
}

IDisposableは必要なときに使う仕組みです。なんでも実装すればよいわけではありません。

9-5. await usingとIAsyncDisposableは何が違うのか

IDisposableは同期的なリソース解放のための仕組みです。

一方、非同期で解放処理を行う必要がある場合には、IAsyncDisposableを使います。

IAsyncDisposableには、DisposeAsync()メソッドがあります。

C#
public interface IAsyncDisposable
{
ValueTask DisposeAsync();
}

利用側では、await usingを使います。

C#
await using var resource = new AsyncResource();

await usingを使うと、スコープを抜けるときにDisposeAsync()が非同期で呼び出されます。

たとえば、非同期I/Oや非同期で閉じる必要がある接続などでは、IAsyncDisposableが使われることがあります。

通常のファイル操作や多くの一般的なリソース解放ではIDisposableusingを使います。非同期の後始末が必要な場合は、IAsyncDisposableawait usingを使う、と考えるとわかりやすいです。

まとめ

C#のIDisposableは、使い終わったリソースを明示的に解放するための重要な仕組みです。

C#にはガベージコレクションがありますが、ファイル、データベース接続、ネットワーク通信、画像、OSハンドルなどのリソースは、GCだけに任せるのではなく、適切なタイミングで解放する必要があります。

IDisposableを実装したオブジェクトは、Dispose()を呼ぶことでリソースを解放できます。ただし、手動でDispose()を書くと呼び忘れや例外時の解放漏れが起きやすいため、基本的にはusing文またはusing宣言を使うのがおすすめです。

C#
using var reader = new StreamReader("sample.txt");

このように書けば、スコープを抜けるときに自動でDispose()が呼ばれます。

自作クラスでIDisposableを実装すべきかどうかは、「解放すべきリソースを自分が所有しているか」で判断します。IDisposableなメンバーを所有している場合や、アンマネージリソースを直接扱う場合は、IDisposableの実装を検討しましょう。

一方で、単なるデータクラスや通常のマネージオブジェクトだけを持つクラスに、無理にIDisposableを実装する必要はありません。

初心者がまず覚えるべきポイントは、次の3つです。

1. IDisposableはリソース解放のための仕組み
2. IDisposableなオブジェクトは基本的にusingで扱う
3. 自作クラスがリソースを所有するならIDisposable実装を検討する

IDisposableDisposeusingを正しく理解すると、ファイル操作やデータベース接続などを安全に扱えるようになります。C#で安定したアプリケーションを作るために、リソースのライフタイムを意識したコードを書くことが大切です。