C# Tips
−usingを使え、使えったら使え(^^)−


[トップ] [目次]

GCとリソース

.NET Frameworkにはガーベジコレクタ(Garbage Collector、以下GC)があります。 そのおかげでオブジェクトを使い終わったあとも破棄のことは考えずにほっといて大丈夫なわけです。
GCは、いろいろとややこしい仕組みで動いているんですが、ものすごく単純に言えば「誰も参照しなくなったらそのうち破棄される」ということです。
たとえば、


Hoge hoge = new Hoge();
// hogeを使う
hoge = null;

こんな風にnullを代入して誰も参照しなくなったらnew Hoge()で生成されたオブジェクトはそのうち破棄されます。
また、


public void Func() {
    Hoge hoge = new Hoge();
    // hogeを使う
}

こんな風にスコープを抜けたとき(Func()からリターンしたとき)も誰も参照しなくなったことになるのでそのうち破棄されます。
この「そのうち」っていうところに注意してください。決してすぐに破棄されるわけじゃありません。 GCはそれなりに大変な処理なので、そんなに頻繁に動きません。 それどころか、.NET FrameworkのGCは「メモリが足りなくなってきたら動く」という感じらしいです。

まぁ、これ自体は何も問題ありません(どっちにしろそのうち破棄されるんだから)。 けど、オブジェクトがメモリ以外のリソースを持っているときは問題になることがあります。
たとえば、


public void Func() {
    FileStream fs = new FileStream("test.txt", FileMode.Open, FileAccess.Read, FileShare.None);
    StreamReader sr = new StreamReader(fs);
    Console.WriteLine(sr.ReadToEnd());
}

こんな場合です(えらく意図的ですが)。
Func()メソッドからリターンした時点でfsやsrを参照しているものは無くなりますから、そのうちGCによって破棄されます。 が、破棄されるまでは"test.txt"はオープンされたままになってしまいます。 FileShare.Noneを指定してオープンしてますから、クローズされるまでは誰もオープンできなくなります(ロックがかかる。FileShare.Noneはそういう指定です)。 ということは、次にいつ"test.txt"がオープンできるかは「GCまかせ」ってことです。 それじゃあ困ります。
StreamReaderクラスには、ちゃんとClose()メソッドが用意されています。 「使い終わったらちゃんとClose()を呼びましょう」  まぁ、それはそれで正解です。けど、.NET Frameworkにはもっと統一的な手法が用意されています。


IDisposable

IDisposableインターフェースがそれです。
IDisposableインターフェースは、上記のような「使い終わったらリソースを開放する必要がある」ということを表現するためにあります。 で、このIDisposableインターフェースには、「使い終わったリソースを開放する」ためのDispose()というメソッドが1つあるだけです。
逆に言うと、「使い終わったらリソースを開放する必要がある」ようなクラスはIDisposableインターフェースを実装しています (普通は。もちろん、そのようなクラスにIDisposableを実装するのは、そのクラスを作る人の責任です。 ひょっとしたらIDisposableを実装してないようなものもあるかもしれません。 標準のクラスライブラリはきちんとIDisposableを実装してますが)。 すなわち、IDisposableを実装しているクラスは「リソースを抱え込んでいるから、使い終わったらDispose()を呼び出してね」 という意思表示をしていると思ったらいいんです。

ようするに、あるクラスがIDisposableを実装していたら、使い終わったあとにとにかくDispose()を呼んでやればいいんです。 「そのクラスはメモリ以外のリソースを使っているのかな?」とかって考える必要はありません。 それに「Close()だっけ?CloseHandle()だっけ?それともCloseFile()だっけ?」とかって悩む必要もありません。 とにかくDispose()です。

ただ、「IDisposableを実装している」と書きましたが、StreamReaderのように直接には実装していない場合もあります。 StreamReaderはその親であるTextReaderがIDisposableを実装しています。 この場合も「IDisposableを実装している」ことになるので注意してください。 それに、クラスリファレンスにはDispose()メソッドが載っていないようです (protectedなDispose(bool disposing)というメソッドは載っていますが、引数なしのpublicなDispose()メソッドは載っていません。 けど、親クラスがIDisposableを実装しているんだから無いはずありません)。


例外に対応する

下記のコードを見てください。 ちゃんと使い終わったらDispose()を呼び出しています。 しかし、コメントのところで例外が発生したらどうなるでしょう?


public void Func() {
    FileStream fs = new FileStream("test.txt", FileMode.Read);
    try {
        StreamReader sr = new StreamReader(fs);
        try {
            // ファイルを読み込んで処理する
            // 処理中に例外が発生したら?
            sr.Dispose();
            fs.Dispose();
        }
        catch () {
            // 例外処理
        }
    }
    catch () {
        // 例外処理
    }
}

コメントのところで例外が出るとDispose()の行に到達することなく、catchに移動してしまいます。 もちろん、メソッドが終了した時点でfs、srを参照するものは無くなりますが、GCが動いてこれらが破棄されるまでファイルはオープンされたままになってしまいます。 これを防ぐために下記のように必ずfinallyでDispose()するようにします。


public void Func() {
    FileStream fs = new FileStream("test.txt", FileMode.Read);
    try {
        StreamReader sr = new StreamReader(fs);
        try {
            // 処理する
        }
        catch () {
            // 例外処理
        }
        finally {
            if (sr != null) {
                sr.Dispose();
            }
        }
    }
    catch () {
        // 例外処理
    }
    finally {
        if (fs != null) {
            fs.Dispose();
        }
    }
}

using

C#には、自動的にDispose()を呼び出してくれ、しかも、例外にも対応してくれる便利な構文があります。 それがusingです。 usingを使うとこんな風に書けます。


public void Func() {
    using (FileStream fs = new FileStream("test.txt", FileMode.Read)) {
        using (StreamReader sr = new StreamReader(fs)) {
            // 処理する
        }
    }
}

このコードは下記のコードとまったく同じ意味です。 実際、これらのコードのILを見比べると100%まったく同じです。


public void Func() {
    FileStream fs = new FileStream("test.txt", FileMode.Read);
    try {
        StreamReader sr = new StreamReader(fs);
        try {
            // 処理する
        }
        finally {
            if (sr != null) {
                sr.Dispose();
            }
        }
    }
    finally {
        if (fs != null) {
            fs.Dispose();
        }
    }
}

ただ、逆に言うとusingは自動的にtry ... finally を作ってくれて、finallyで必ずDispose()を呼び出すようにしてくれるだけです。 なので、例外に対処するために下記のようなコードになることが多いでしょう。


public void Func() {
    using (FileStream fs = new FileStream("test.txt", FileMode.Read)) {
        using (StreamReader sr = new StreamReader(fs)) {
            try {
                // 処理する
            }
            catch () {
                // 例外処理
            }
        }
    }
}

usingの多段重ね

上記のようにusingがネストしているときは、下記のように書くことができます。


public void Func() {
    using (FileStream fs = new FileStream("test.txt", FileMode.Read))
    using (StreamReader sr = new StreamReader(fs)) {
        try {
            // 処理する
        }
        catch () {
            // 例外処理
        }
    }
}

1つ目のusingの末尾に";"も"{"も無いところに注意してください。
構文的にはなんとも気持ち悪いですが、結構便利です。 また、できあがったILは普通にネストして書いたときとまったく同じです。


とにかくusingを使え

そんなわけで、とにかく使えるところでは必ずusingを使うようにしましょう。
また、メンバに格納するなどusingを使えない場合は、必ず使い終わった後にDispose()するようにしましょう。


[トップ] [目次]

株式会社ディーバ 青柳 臣一
2002/11/11