PG日誌

各記事はブラウザの横幅を1410px以上にすると2カラムの見出しが表示されます。なるべく横に広げてみてください。

C#はコンストラクタで例外を投げても良いのか?

コンストラクタで例外を投げるとメモリリークする、なんて聞いたことがある人も多いかと思いますがC#でコンストラクタで例外を投げるのはOK/NGのどちらでしょうか?結論は、コンストラクタ内で例外を投げる場合コンストラクタ内で確保したリソースはコンストラクタ内で開放処理を実装すれば投げてもOKです。

ファイナライザーと Dispose は正常に作成されたオブジェクトにのみ有効でコンストラクターが正常終了しなかったオブジェクトに対しては動作しません。従って、以下の例の実装だとプログラムが終了するまでリソースが解放されないリークが発生します。

// コンストラクターで例外が発生するクラス
public class Foo : IDisposable
{
    private MyClass m;
    private FileStream fs;

    public Foo(string path)
    {
        // 自作のクラスの作成して★フィールドに保存する
        this.m = new MuClass();
        // 排他ロックでファイルを開いて★フィールドに保存する
        this.fs = new FileStream(path, FileMode.Open, FileAccess.Read, FileShare.None);

        // ★ここで例外発生
        throw new MyException();
    }

    ~Foo()
    {
        Console.WriteLine("Call ~Foo()");
        this.Dispose();
    }

    public void Dispose()
    {
        Console.WriteLine("Call Dispose()");
        using (this.sr) { }
        using (this.fs) { }
        GC.SuppressFinalize(this);
    }
}

// ↑ のクラスを ↓ のように使用すると開放されない
publi void Test()
{
    try
    {
        using(var f = new Foo())
        {
            // ...
        }
        // コンストラクタ内で例外が起きた場合
        // Diposeメソッドは呼び出されない(= f が null だから)
    }
    finally
    {
        GC.Collect(); // 強制的にGCを起動する
        // Fooのファイナライザーが呼ばれない
        // → インスタンスが正常に作られなかったため呼び出されない
    }
}

特にリソースを確保した後に、メンバーに代入するとフィールドに設定されたオブジェクトは解放が怪しい状態になります。したがってコンストラクタ内で例外が起きる可能性がある場合は、以下のようにコンストラクタ内で例外を catch し開放処理を実装します。併せてフィールドに保存した値は null を入れておきましょう。

// 解放処理をコンストラクタ内に記述する
public Foo(string path)
{
    try
    {
        this.m = new MyClass();
        // 排他ロックでファイルを開く
        this.fs = new FileStream(path, FileMode.Open, FileAccess.Read, FileShare.None);
        // ファイルを開いた後に例外を起こす
        throw new MyException();
    }
    catch (Exception)
    //catch (Exception ex) when (ex is MyException || ex is IOException)
    {
        // (1) nullを代入してオブジェクト参照を捨てる
        this.m = null;

        // (2) Disposeを継承しているオブジェクトの場合Disposeを呼び出して
        //     リソースを開放した後に nullを代入して参照を捨てる
        using (fs) { }
        this.fs = null;
        
        // (3) 上位の例外を伝えるため再スローする
        throw;
    }
}

// 最後に代入をまとめて行う
//  → アンマネージリソースを扱わない場合これでよい場合もある
public Foo(string path)
{
    // 自作のクラスの作成してローカル変数にとっておく
    var _m = new MyClass();
    // 排他ロックでファイルを開いてローカル変数に保存する
    var _fs = new FileStream(path, FileMode.Open, FileAccess.Read, FileShare.None);
    // ファイルを開いた後に例外を起こす
    throw new MyException();

    // 安全な位置に来たらメンバーに代入する
    this.m = _m;
    this.fs = _fs;
}

特に、例外が発生して生成が未完了のオブジェクトのメンバー変数に保存されたオブジェクトはリソースリークの可能性が非常に高い(強い参照関係があるのか?(要確認))、メンバー変数にnullを代入したりしないと代入したオブジェクトのファイナライザーが呼び出されなかったりするので注意が必要です。

実装時にオブジェクト数が多くなった場合、考慮事項が多くなりがちで実装が大変なので例外が基本的に発生しないコードを記述する、もしくは外部から生成済みのオブジェクトを与えて依存関係を注入するなどで通常稼働時は例外が発生しないようにしたほうが良さそうです。

参考

本件詳細に述べている記事(英語)が以下にあります。2008年と書かれた年が古いですが現在でも通用する内容です。もしよければこちらも参照ください。

Constructor Exceptions in C++, C#, and Java

https://herbsutter.com/2008/07/25/constructor-exceptions-in-c-c-and-java/