Menu

C#のusingステートメント:IDisposable、Dispose、using var

usingステートメントは、例外が投げられた場合でも、ファイル、ストリーム、接続などのIDisposableなオブジェクトでDisposeが呼ばれることを保証します。それが何にコンパイルされるか、オブジェクトが破棄される順序、C# 8のusing宣言、そして自分のクラスでIDisposableを実装する方法を学びます。

このページのコードはエディタで実行できます - 編集してすぐに結果を確認できます。

一部のオブジェクトは、ガベージコレクターが管理しないリソースを保持しています。開いたファイル、ネットワークソケット、データベース接続、オペレーティングシステムのハンドルなどです。それらは IDisposable を実装していて、Dispose() を呼ぶことでリソースを解放します。using ステートメントは、例外が投げられた場合でも、決まった地点でそれを呼んでくれます。

このページは using ステートメントについてのものです。ファイルの先頭にある using System; の行は、名前空間をインポートするusingディレクティブという別の機能です。

usingブロック

using (declaration) { body } はオブジェクトを作り、本体を実行してから Dispose を呼びます。Dispose で表示を行う小さなクラスを使うと、それがいつ起きるかが正確にわかります。

出力:

open db
db sends SELECT 1
close db
open api
api sends GET /orders
close api
error handled

2つ目のブロックは例外を投げますが、catch が例外を処理する前に「close api」が表示されます。この保証こそ、最後の行で自分で Dispose() を呼ぶのではなく using を使う理由です。手動の呼び出しは、その前に投げられたどんな例外によっても飛ばされてしまいます。

usingが何にコンパイルされるか

コンパイラはusingブロックを try/finally に展開します。次の2つは同等です。

using (var writer = new StreamWriter("log.txt"))
{
    writer.WriteLine("started");
}

// is compiled as:
{
    var writer = new StreamWriter("log.txt");
    try
    {
        writer.WriteLine("started");
    }
    finally
    {
        if (writer != null) ((IDisposable)writer).Dispose();
    }
}

この展開から3つのことがわかります。変数のスコープはブロックに限られ、ブロックの中では読み取り専用です(writer に再代入はできません)。オブジェクトは IDisposable を実装していなければならず、そうでなければコードはコンパイルできません。そして null の値も許されます。その場合 Dispose は単に飛ばされるので、ファクトリーが何も返さないかもしれない場合に便利です。

ファイルとストリーム

ファイルとストリームのクラスは日常的な例です。StreamWriter はテキストをメモリにバッファーし、Dispose がバッファーをディスクに書き出してファイルハンドルを閉じます。

出力:

id,total
1001,59.90
1002,12.50

Dispose がないと、ライターの最後の行がまだバッファーに残っているかもしれず、Windowsでは開いたハンドルのせいで、他のプログラム(や自分の次の File.Delete)がファイルに触れられなくなります。File.WriteAllText や File.ReadAllLines のようなヘルパーは内部で開いて破棄するので、using は不要です。詳しくはファイルを参照してください。

複数のリソース:破棄の順序

間に波かっこを挟まずに using ステートメントを重ねると、1つのブロックで複数のリソースを開けます。それらは作成とは逆の順序で破棄されます。

出力:

acquire file
acquire buffer
acquire writer
working
release writer
release buffer
release file
acquire a
acquire b
both open
release b
release a

逆順は、層になったリソースが必要とする順序です。ライターはバッファーをラップし、バッファーはファイルをラップしているので、ファイルが閉じる前に、バッファーがファイルに書き込み、その前にライターがバッファーへ書き出さなければなりません。

using宣言(C# 8)

C# 8では波かっこのない形が追加されました。変数は、それを囲むスコープが終わるとき、通常はメソッドの終わりに破棄されます。

static void ExportReport(string path, IEnumerable<string> rows)
{
    using var writer = new StreamWriter(path);
    writer.WriteLine("Report");

    foreach (string row in rows)
    {
        writer.WriteLine(row);
    }
} // writer.Dispose() runs here, at the end of the method

インデントが1段減り、メソッドが2つか3つのリソースを使う場合には大きな差になります。破棄の順序のルールは同じで、1つのスコープの宣言は逆の順序で破棄されます。欠点は、スコープが終わるまでリソースが開いたままになることです。メソッドがさらに処理を行う前にファイルを閉じるべきなら、ブロックの形を使うか、宣言を独自の { } ブロックに入れます。

後片付けが非同期のオブジェクト(多くのデータベース接続やストリームのような IAsyncDisposable)のために、C# 8には await using もあり、同じ地点で DisposeAsync() をawaitします。

自分のクラスでIDisposableを実装する

クラスが破棄可能なものを所有している(ストリーム、タイマー、接続を作った)ので、それを破棄しなければならないときは、IDisposable を実装します。よくあるケースなら、短いメソッドで済みます。

出力:

1. user ana logged in
2. order 1001 created
AuditLog disposed
Caught ObjectDisposedException

これが従っているルールは次のとおりです。Dispose はオブジェクトが所有するものをすべて破棄します。2回以上呼ばれても2回目以降は何もしません。破棄した後にオブジェクトを使うと ObjectDisposedException が投げられます。using (log) からは、ブロックが宣言の代わりに既存の変数を受け取れることもわかります。

protected virtual void Dispose(bool disposing) メソッドとファイナライザーを持つ、より長い「Disposeパターン」が必要なのは、クラスがアンマネージドなハンドル(ネイティブコードからの生のポインター)を直接保持する場合だけです。現代の.NETでは、そうしたハンドルは代わりに SafeHandle のサブクラスでラップし、自分のクラスには上の単純な版だけがあれば十分です。

よくある間違い

  • using ブロックの中で作ったオブジェクトを返す。 ブロックを抜けるときに破棄されるので、呼び出し側は閉じたストリームを受け取ります。データを返すか、呼び出し側に using を持たせます。
  • ストリームやライターで using を忘れる。 バッファーのデータが失われることがあり、ファイルはロックされたままになります。
  • 共有のオブジェクトを破棄する。 破棄するのは自分が所有するものだけにします。たとえば寿命の長い HttpClient は、リクエストごとに作って破棄するのではなく、共有して再利用するためのものです。
  • ブロックの後でオブジェクトを使う。 破棄されたオブジェクトのメソッドは ObjectDisposedException を投げます。
  • ガベージコレクターに頼る。 ガベージコレクターが解放するのはメモリであってファイルやソケットではなく、Dispose は決して呼びません。

よくある質問

C#のusingステートメントは何をしますか?

using (var x = ...) { ... } は、ブロックが正常に終わった場合も、return で抜けた場合も、例外が投げられた場合も、ブロックの終わりで x.Dispose() を呼びます。コンパイラはこれを、finally で Dispose を呼ぶ try/finally に変換するので、リソースはガベージコレクターが動くいつかの時点ではなく、決まった地点で解放されます。

C#のIDisposableとは何ですか?

IDisposable は void Dispose() という1つのメソッドを持つインターフェースです。ファイルハンドル、ネットワークソケット、データベース接続、タイマーなど、明示的に解放しなければならないものを保持するクラスが実装します。型が IDisposable を実装しているオブジェクトは、使い終わったら、通常は using で破棄すべきです。

usingとusing varの違いは何ですか?

using (var x = ...) { } はブロックの終わりで破棄します。using宣言 using var x = ...;(C# 8以降)は独自のブロックを持たず、それを囲むスコープが終わるとき、通常はメソッドの終わりに破棄します。リソースをメソッドの残りの間ずっと使う場合に、ネストを減らせます。

複数のusingのオブジェクトはどの順序で破棄されますか?

作成とは逆の順序です。using (var a = ...) using (var b = ...) { } では、b が先に破棄され、次に a が破棄されます。FileStream をラップする StreamWriter のように、b が a に依存している場合に望ましい順序です。

ガベージコレクターはDisposeを呼びますか?

呼びません。ガベージコレクターはメモリを解放し、予測できない後の時点でファイナライザーを実行することはありますが、Dispose は決して呼びません。破棄されずに残ったファイルはロックされたままになることがあり、バッファーのデータが書き込まれないこともあります。だから using が存在します。

Coddy programming languages illustration

Coddyでコードを学ぼう

始める