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

C#はコンストラクター内で例外を投げても問題ありません。

以上。ってだけだと記事が終わってしまうので、以下に補足説明を書いていきたいと思います。

コンストラクターで例外を投げても問題ありませんが、コンストラクターでアンマネージリソースを確保したとか、マネージリソースでもハンドルをGC任せではなく即座に開放したい場合(コンストラクターが例外になったら大抵即時解放になると思いますが)の場合、正しく try~catch で開放処理しましょうねという補足説明をしたいと思います。

あと、コンストラクターが失敗するとオブジェクトは生成失敗になり、ファイナライザーのキューに登録されない(=ファイナライザーは呼ばれない)ためファイナライザーで開放処理を実装しているからコンストラクター内で例外が発生した時にcatch節の開放処理を実装しなくてよいというのは誤りです。

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

    public Foo(string path)
    {
        // コンストラクター内で排他ロックのファイルオープン
        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メソッドは呼び出されない
    }
    finally
    {
        GC.Collect();
        // ★Fooのファイナライザーは呼ばれない
        // → インスタンスが正常に作られなかったため呼び出されない
    }
}

この場合、マネージリソースはしばらくすれば解放されますが、いつ解放されるのか制御できないので開放処理を書くほうが望ましいです。

また、以下例のようにアンマネージリソースを確保した後に例外が発生すると解放されないため開放を行っている例です。必ず catch して解放処理を書くようにしましょう。

[DllImport("kernel32.dll", SetLastError = true)]
private static extern IntPtr OpenFile(string lpFileName);
[DllImport("kernel32.dll", SetLastError = true)]
private static extern bool CloseHandle(IntPtr hObject);

IntPtr _fileHandle = IntPtr.Zero;

// 解放処理をコンストラクター内に記述する
public Foo(string path)
{
    try
    {
        // アンマネージリソースを確保する処理がコンストラクター内に存在する
        _fileHandle = OpenFile(dialog.FileName);

        // ファイルを開いた後に例外を起こす
        throw new MyException();
    }
    catch (MyException)
    {
        // 解放処理を必ず記述する
        if(_fileHandle != IntPtr.Zero
        {
            CloseHandle(_fileHandle)
        }

        throw; // 上位の例外を伝えるため再スローする
    }
}

現実的にはコンストラクター内でリソース確保を何個もするケースでは、オブジェクトの生成は安全に終わらせて別の Initialize() メソッドで初期化を行う方が設計としては良いです。コンストラクター内で色々やって例外がでて結局オブジェクトが生成できません、catch 節で色々考慮して複雑な開放処理を実装しますは手間だし何より複雑化してバグの原因になりがちです。

いずれにしろ適切にリソースが解放できるならコンストラクター内で例外を投げても問題ありません。

参考

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

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

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