例外の使用

C# では、例外と呼ばれるメカニズムを使用して、プログラムの実行時に発生したエラーがプログラムに伝えられます。 例外は、エラーが発生したコードによってスローされ、エラーを修正できるコードによってキャッチされます。 例外をスローできるのは、.NET ランタイム、またはプログラム内のコードです。 スローされた例外は、例外の catch ステートメントが見つかるまで呼び出し履歴をさかのぼります。 キャッチされない例外は、システムが提供する汎用の例外ハンドラーによって処理されます。このとき、ダイアログ ボックスが表示されます。

例外は、Exception から派生したクラスによって表されます。 このクラスは例外の型を識別し、例外に関する詳細情報が含まれたプロパティを保持します。 例外をスローするには、例外の派生クラスのインスタンスを作成し、必要に応じて例外のプロパティを設定してから、throw キーワードを使用してオブジェクトをスローする必要があります。 次に例を示します。

class CustomException : Exception
{
    public CustomException(string message)
    {
    }
}
private static void TestThrow()
{
    throw new CustomException("Custom exception in TestThrow()");
}

例外がスローされると、ランタイムは、現在のステートメントが try ブロック内に存在するかどうかを確認します。 存在する場合は、try ブロックに関連付けられている catch ブロックをチェックして、例外をキャッチできるかどうかを確認します。 通常は、この Catch ブロックによって例外の型が指定されます。catch ブロックの型が、例外または例外の基底クラスの型と一致する場合、catch ブロックはメソッドを処理できます。 次に例を示します。

try
{
    TestThrow();
}
catch (CustomException ex)
{
    System.Console.WriteLine(ex.ToString());
}

例外をスローするステートメントが try ブロック内にない場合、または try ブロックにそれが含まれていても、対応する catch ブロックが存在しない場合は、ランタイムによって呼び出し側のメソッドで try ステートメントと catch ブロックが確認されます。 続けて、呼び出し履歴で、対応する catch ブロックを探します。 catch ブロックが見つかり実行されると、その catch ブロックの後にある次のステートメントに制御が渡されます。

try ステートメントには、複数の catch ブロックを含めることができます。 例外を処理できる最初の catch ステートメントが実行され、その後の catch ステートメントは、対応していても無視されます。 catch ブロックを、最も具体的なもの (または最も派生したもの) から最も具体的でないものの順に並べます。 次に例を示します。

using System;
using System.IO;

namespace Exceptions
{
    public class CatchOrder
    {
        public static void Main()
        {
            try
            {
                using (var sw = new StreamWriter("./test.txt"))
                {
                    sw.WriteLine("Hello");
                }
            }
            // Put the more specific exceptions first.
            catch (DirectoryNotFoundException ex)
            {
                Console.WriteLine(ex);
            }
            catch (FileNotFoundException ex)
            {
                Console.WriteLine(ex);
            }
            // Put the least specific exception last.
            catch (IOException ex)
            {
                Console.WriteLine(ex);
            }
            Console.WriteLine("Done");
        }
    }
}

catch ブロックが実行される前に、ランタイムにより finally ブロックがチェックされます。 Finally ブロックを使用すると、中止された try ブロックによって残ることがある、あいまいな状態をクリーンアップできます。また、ランタイムのガベージ コレクターによってオブジェクトが終了されるのを待たずに外部リソース (グラフィック ハンドル、データベース接続、ファイル ストリームなど) を解放することもできます。 次に例を示します。

static void TestFinally()
{
    FileStream? file = null;
    //Change the path to something that works on your machine.
    FileInfo fileInfo = new System.IO.FileInfo("./file.txt");

    try
    {
        file = fileInfo.OpenWrite();
        file.WriteByte(0xF);
    }
    finally
    {
        // Closing the file allows you to reopen it immediately - otherwise IOException is thrown.
        file?.Close();
    }

    try
    {
        file = fileInfo.OpenWrite();
        Console.WriteLine("OpenWrite() succeeded");
    }
    catch (IOException)
    {
        Console.WriteLine("OpenWrite() failed");
    }
}

WriteByte() から例外がスローされた場合、file.Close() が呼び出されなければ、ファイルを再度開こうとする 2 番目の try ブロックのコードが失敗し、ファイルはロックされたままになります。 例外がスローされても finally ブロックは実行されるため、上の例の finally ブロックではファイルを適切に閉じて、エラーを回避できます。

例外がスローされた後、対応する catch ブロックが呼び出し履歴に見つからない場合は、次のいずれかが発生します。

  • 例外がファイナライザーの内部で発生した場合、ファイナライザーは中止され、基本ファイナライザー (存在する場合) が呼び出されます。
  • 呼び出し履歴に静的コンストラクターまたは静的フィールド初期化子が含まれている場合は、TypeInitializationException がスローされ、新しい例外の InnerException プロパティに元の例外が割り当てられます。
  • スレッドの開始位置に到達すると、スレッドは終了します。