1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

usingはいつDisposeを呼ぶのか:例外・using宣言・GCを実験で確かめる

1
Posted at

はじめに

using の中で例外が出たとき、Dispose は呼ばれるのでしょうか。呼ばれるなら、外側の catch より先でしょうか、後でしょうか。

この記事では、この問いに加えて「using宣言ではいつ呼ばれるか」「呼び忘れたら GC が代わりに呼ぶか」の3つを、コンソールアプリで実験して確かめます。

1. usingの中で例外が出たとき

コード

try
{
    using (var r = new MyResource())
    {
        Console.WriteLine("ブロック内");
        throw new Exception("失敗");
    }
}
catch (Exception e)
{
    Console.WriteLine($"catch: {e.Message}");
}

class MyResource : IDisposable
{
    public MyResource() => Console.WriteLine("取得");

    public void Dispose() => Console.WriteLine("Dispose");
}

問
出力結果はどうなるか?

回答

取得
ブロック内
Dispose
catch: 失敗

usingはtry-finallyに書き換わる

実行する前は「取得 → ブロック内 → catch」と出て、Dispose は出ないと予想していました。using はブロックの最後まで進んだときに Dispose を呼ぶもので、途中で throw されたら、そのまま呼び出し元に戻ると考えていたためです。

実際には Dispose が呼ばれ、しかも catch より先に出ました。

using は、コンパイル時に try-finally に書き換えられます。確かめるために、using を使わずに同じ動きを自分で書いてみました。

var r = new MyResource();

try
{
    try
    {
        Console.WriteLine("ブロック内");
        throw new Exception("失敗");
    }
    finally
    {
        r.Dispose();
    }
}
catch (Exception e)
{
    Console.WriteLine($"catch: {e.Message}");
}
取得
ブロック内
Dispose
catch: 失敗

using 版と同じ出力になりました。

最初は、catch と finally を同じ try に付けて書いていました。

var r = new MyResource();

try
{
    Console.WriteLine("ブロック内");
    throw new Exception("失敗");
}
catch (Exception e)
{
    Console.WriteLine($"catch: {e.Message}");
}
finally
{
    r.Dispose();
}
取得
ブロック内
catch: 失敗
Dispose

こちらは catch が先になります。

違いは catch の位置です。例外は、型が合う一番近い catch まで外側へ伝わり、その途中にある finally を必ず通ります。using が作るのは finally だけで、catch は作りません。そのため、例外は using の中では止まらず、Dispose を通ってから外側の catch に届きます。これが Dispose が catch より先に出る理由です。

newがtryの手前にある理由

書き換えたコードでは、new MyResource() が try の中ではなく手前にあります。

コンストラクタの中で例外が出た場合、インスタンスはできておらず、解放すべきものがまだ手に入っていません。try が守るのは「取得に成功した後」の部分だけで、取得に成功したものだけを finally で解放する形になっています。なお、実際にコンパイラが生成するコードでは、finally の中で null チェックをしてから Dispose を呼んでいます。

2. using宣言のとき

C# 8 からは、{ } を付けずに using var r = ...; と書く using宣言が使えます。ブロック形式と比べました。ここからは、MyResource を名前を受け取る形に変えています。

コード

Block();
Console.WriteLine("---");
Declaration();
Console.WriteLine("全体の終了");

void Block()
{
    using (var r = new MyResource("A"))
    {
        Console.WriteLine("A 使用");
    }
    Console.WriteLine("Block の後続処理");
}

void Declaration()
{
    using var r = new MyResource("B");
    Console.WriteLine("B 使用");
    Console.WriteLine("Declaration の後続処理");
}

class MyResource : IDisposable
{
    private readonly string _name;

    public MyResource(string name)
    {
        _name = name;
        Console.WriteLine($"{_name} 取得");
    }

    public void Dispose() => Console.WriteLine($"{_name} Dispose");
}

問
「A Dispose」と「B Dispose」は、それぞれどこに出るか?

回答

A 取得
A 使用
A Dispose
Block の後続処理
---
B 取得
B 使用
Declaration の後続処理
B Dispose
全体の終了

Disposeは、変数を囲む一番内側の { } を抜けるときに呼ばれる

最初は「B Dispose」が「B 取得」の直後に出ると予想していました。using var r = ...; の行で using が終わると考えたためです。

外れたので、次は「ファイル内のすべての処理が終わったとき」だと考えました。それを確かめるために Console.WriteLine("全体の終了"); を足したところ、「B Dispose」は「全体の終了」より先に出ました。

using宣言の Dispose は、その変数を囲んでいる一番内側の { } を抜けるときに呼ばれます。今回はそれが Declaration メソッドでした。if や for の { } の中で宣言すれば、そのブロックを抜けるときに呼ばれます。

using宣言はネストが浅くなって読みやすい一方で、メソッドの最後まで解放されません。長いメソッドの途中で早めに手放したいもの(ファイルやDB接続など)には、ブロック形式が向いています。

3. Disposeを呼び忘れたとき

コード(MyResource は2と同じ)

Forget();
GC.Collect();
GC.WaitForPendingFinalizers();
Console.WriteLine("GCを強制的に走らせた");

void Forget()
{
    var r = new MyResource("C");
    Console.WriteLine("C 使用(Disposeを呼ばずに終わる)");
}

GC.Collect() はふだんは書かないものですが、GC が走ったことをはっきりさせるために、実験として強制的に呼んでいます。

問
「C Dispose」は出るか?

回答

C 取得
C 使用(Disposeを呼ばずに終わる)
GCを強制的に走らせた

GCはDisposeを呼ばない

GC を強制的に走らせても、Dispose は呼ばれませんでした。

Dispose 自体は、中身の決まっていないただのメソッドです。何をするかは、そのクラスを作った人が決めます。実際のクラスでは、多くの場合「.NET のメモリの外にあるもの」を返す処理が書かれています。たとえば FileStream の Dispose は OS に開いてもらったファイルを閉じ、DB接続の Dispose は接続をプールに返します。

これらは OS や DB の側で持っているものなので、GC からは見えません。GC が回収するのは、オブジェクトが使っていたメモリだけです。オブジェクトのメモリが回収されても、OS 側のファイルが閉じられるとは限りません。

もう1つの理由は、GC がいつ走るかをプログラムから決められないことです。ファイルや接続を使い終わった時点で確実に返すには、自分で Dispose を呼ぶ(using で囲む)必要があります。

なお、GC が呼ぶ可能性があるのは、~MyResource() のように書くファイナライザです。ファイナライザはいつ動くかが決まっておらず、呼び忘れたときの保険として使われるもので、Dispose の代わりにはなりません。今回の MyResource にはファイナライザを書いていないので、何も呼ばれていません。

参考

1
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?