次の方法で共有


例外のベスト プラクティス

アプリケーションの信頼性には、適切な例外処理が不可欠です。 予期される例外を意図的に処理して、アプリのクラッシュを防ぐことができます。 ただし、クラッシュしたアプリは、未定義の動作を持つアプリよりも信頼性が高く、診断可能です。

この記事では、例外の処理と作成に関するベスト プラクティスについて説明します。

例外の処理

次のベスト プラクティスは、例外の処理方法に関するものです。

try/catch/finally ブロックを使用してエラーから復旧するか、リソースを解放します

例外を生成する可能性があるコードの場合、アプリがその例外から復旧できる場合は、コードの周囲 try/catch ブロックを使用します。 catch ブロックでは、常に、最も派生した例外から最小派生の例外を並べ替える必要があります。 (すべての例外は、Exception クラスから派生します。より多くの派生例外は、基底例外クラスのcatch句の前にあるcatch句では処理されません)。コードが例外から回復できない場合は、その例外をキャッチしないでください。 可能であれば、コールスタックの上位にあるメソッドでリカバリーできるようにします。

using ステートメントまたは finally ブロックで割り当てられたリソースをクリーンアップします。 例外がスローされたとき、リソースを自動クリーンアップするには using ステートメントを選択します。 finally ブロックを使用して、IDisposableを実装していないリソースをクリーンアップします。 finally 句のコードは常に、例外がスローされるときでも実行されます。

例外を回避するために一般的な条件を処理する

発生する可能性はありますが、例外をトリガーする可能性がある条件については、例外を回避する方法で処理することを検討してください。 たとえば、既に閉じている接続を閉じようとすると、 InvalidOperationExceptionが表示されます。 これを回避するには、if ステートメントを使用して、接続状態を確認してから閉じるようにしてください。

if (conn.State != ConnectionState.Closed)
{
    conn.Close();
}
If conn.State <> ConnectionState.Closed Then
    conn.Close()
End IF

閉じる前に接続状態を確認しない場合は、 InvalidOperationException 例外をキャッチできます。

try
{
    conn.Close();
}
catch (InvalidOperationException ex)
{
    Console.WriteLine(ex.GetType().FullName);
    Console.WriteLine(ex.Message);
}
Try
    conn.Close()
Catch ex As InvalidOperationException
    Console.WriteLine(ex.GetType().FullName)
    Console.WriteLine(ex.Message)
End Try

選択する方法は、イベントが発生する頻度によって異なります。

  • イベントが頻繁に発生しない場合、つまりイベントが本当に例外的で、予期しないファイルの終わりなどのエラーを示す場合は、例外処理を使用します。 例外処理を使用すると、通常の条件で実行されるコードが少なくなります。

  • イベントが定期的に発生し、通常の実行の一部と見なされる可能性がある場合は、コード内のエラー状態を確認します。 一般的なエラー状態を確認すると、例外を回避するため、実行されるコードが少なくなります。

    事前チェックでは、ほとんどの場合、例外が排除されます。 ただし、チェックと操作の間で保護された条件が変化する競合状態が存在する場合があり、その場合も例外が発生する可能性があります。

Try*メソッドを呼び出して例外を回避する

例外のパフォーマンス コストが非常に大きい場合、一部の .NET ライブラリ メソッドでは、別の形式のエラー処理が提供されます。 たとえば、Int32.Parseが解析する値が大きすぎてOverflowExceptionで表現できない場合には、Int32をスローします。 しかし、Int32.TryParse はこの例外をスローしません。 代わりに、ブール値を返し、成功した場合に解析された有効な整数を含む out パラメーターを持っています。 Dictionary<TKey,TValue>.TryGetValue は、ディクショナリから値を取得しようとする場合にも同様の動作をします。

取り消しと非同期例外に対応する

非同期メソッドを呼び出すときに、OperationCanceledExceptionから派生するTaskCanceledExceptionではなく、OperationCanceledExceptionをキャッチすることをお勧めします。 多くの非同期メソッドは、取り消しが要求された場合に OperationCanceledException 例外をスローします。 これらの例外により、キャンセル要求が検出されると、実行を効率的に停止し、コールスタックをアンワインドすることができます。

非同期メソッドは、実行中にスローされる例外を、返されるタスク内に格納します。 返されたタスクに例外が格納されている場合、その例外はタスクが待機しているときにスローされます。 ArgumentException などの使用に関する例外は、引き続き同期的にスローされます。 詳細については、「 非同期例外」を参照してください。

例外を回避できるようにクラスを設計する

クラスは、例外をトリガーする呼び出しを回避できるメソッドまたはプロパティを提供できます。 たとえば、 FileStream クラスは、ファイルの末尾に達したかどうかを判断するのに役立つメソッドを提供します。 これらのメソッドを呼び出すことで、ファイルの末尾を超えて読み取った場合にスローされる例外を避けることができます。 次の例は、例外をトリガーせずにファイルの末尾まで読み取る方法を示しています。

class FileRead
{
    public static void ReadAll(FileStream fileToRead)
    {
        ArgumentNullException.ThrowIfNull(fileToRead);

        int b;

        // Set the stream position to the beginning of the file.
        fileToRead.Seek(0, SeekOrigin.Begin);

        // Read each byte to the end of the file.
        for (int i = 0; i < fileToRead.Length; i++)
        {
            b = fileToRead.ReadByte();
            Console.Write(b.ToString());
            // Or do something else with the byte.
        }
    }
}
Class FileRead
    Public Sub ReadAll(fileToRead As FileStream)
        ' This if statement is optional
        ' as it is very unlikely that
        ' the stream would ever be null.
        If fileToRead Is Nothing Then
            Throw New System.ArgumentNullException()
        End If

        Dim b As Integer

        ' Set the stream position to the beginning of the file.
        fileToRead.Seek(0, SeekOrigin.Begin)

        ' Read each byte to the end of the file.
        For i As Integer = 0 To fileToRead.Length - 1
            b = fileToRead.ReadByte()
            Console.Write(b.ToString())
            ' Or do something else with the byte.
        Next i
    End Sub
End Class

例外を回避するもう 1 つの方法は、例外をスローするのではなく、最も一般的なエラー ケースに対して null (または既定) を返す方法です。 一般的なエラー ケースは、通常の制御フローと見なすことができます。 このような場合に null (または既定) を返すことで、アプリへのパフォーマンスへの影響を最小限に抑えることができます。

値型の場合は、アプリのエラー インジケーターとして Nullable<T>default のどちらを使用するかを検討してください。 Nullable<Guid>を使用すると、defaultnullではなくGuid.Emptyになります。 場合によっては、 Nullable<T> を追加すると、値が存在する場合や存在しない場合がより明確になる場合があります。 また、 Nullable<T> を追加すると、追加のケースを作成して不要であることを確認し、潜在的なエラーの原因を作成するためだけに役立つ場合もあります。

例外が原因でメソッドが完了しない場合の復元状態

呼び出し元が、メソッドから例外がスローされるときに副作用が発生しないと仮定できる必要があります。 たとえば、ある口座から現金を引き出して別の口座に預金することで送金を行うコードがあって、預金の実行中に例外がスローされた場合、引き出しが有効のままにしておきたくはないはずです。

public void TransferFunds(Account from, Account to, decimal amount)
{
    from.Withdrawal(amount);
    // If the deposit fails, the withdrawal shouldn't remain in effect.
    to.Deposit(amount);
}
Public Sub TransferFunds(from As Account, [to] As Account, amount As Decimal)
    from.Withdrawal(amount)
    ' If the deposit fails, the withdrawal shouldn't remain in effect.
    [to].Deposit(amount)
End Sub

上記のメソッドでは例外を直接スローしません。 ただし、デポジット操作が失敗した場合に引き出しが取り消されるように、メソッドを記述する必要があります。

この状況に対処する方法の 1 つは、預金トランザクションによってスローされた例外をキャッチし、引き出しをロールバックすることです。

private static void TransferFunds(Account from, Account to, decimal amount)
{
    string withdrawalTrxID = from.Withdrawal(amount);
    try
    {
        to.Deposit(amount);
    }
    catch
    {
        from.RollbackTransaction(withdrawalTrxID);
        throw;
    }
}
Private Shared Sub TransferFunds(from As Account, [to] As Account, amount As Decimal)
    Dim withdrawalTrxID As String = from.Withdrawal(amount)
    Try
        [to].Deposit(amount)
    Catch
        from.RollbackTransaction(withdrawalTrxID)
        Throw
    End Try
End Sub

この例では、 throw を使用して元の例外を再スローする方法を示しています。これにより、呼び出し元は、 InnerException プロパティを調べることなく、問題の実際の原因を簡単に確認できます。 別の方法として、新しい例外をスローし、元の例外を内部例外として含めます。

catch (Exception ex)
{
    from.RollbackTransaction(withdrawalTrxID);
    throw new TransferFundsException("Withdrawal failed.", innerException: ex)
    {
        From = from,
        To = to,
        Amount = amount
    };
}
Catch ex As Exception
    from.RollbackTransaction(withdrawalTrxID)
    Throw New TransferFundsException("Withdrawal failed.", innerException:=ex) With
    {
        .From = from,
        .[To] = [to],
        .Amount = amount
    }
End Try

例外を適切に捕捉して再スローする

例外がスローされると、その情報の一部はスタック トレースになります。 スタック トレースとは、例外をスローするメソッドで始まり、例外をキャッチするメソッドで終了する、メソッド呼び出しを階層化した一覧です。 たとえば、 throw ステートメントで例外を指定して例外を再スローすると、 throw e 現在のメソッドでスタック トレースが再開され、例外をスローした元のメソッドと現在のメソッドの間のメソッド呼び出しの一覧が失われます。 元のスタック トレース情報を例外と共に保持するには、例外を再スローする場所に応じて 2 つのオプションがあります。

  • 例外インスタンスをキャッチしたハンドラー (catch ブロック) 内から例外を再スローする場合は、例外を指定せずに throw ステートメントを使用します。 コード分析規則 CA2200 は、 スタック トレース情報が誤って失われる可能性があるコード内の場所を見つけるのに役立ちます。
  • ハンドラー (catch ブロック) 以外の場所から例外を再スローする場合は、ExceptionDispatchInfo.Capture(Exception) を使用してハンドラー内の例外をキャプチャし、再スローする場合は ExceptionDispatchInfo.Throw() を使用します。 ExceptionDispatchInfo.SourceException プロパティを使用して、キャプチャされた例外を調べることができます。

次の例は、 ExceptionDispatchInfo クラスを使用する方法と、出力がどのようになるかを示しています。

ExceptionDispatchInfo? edi = null;
try
{
    var txt = File.ReadAllText(@"C:\temp\file.txt");
}
catch (FileNotFoundException e)
{
    edi = ExceptionDispatchInfo.Capture(e);
}

// ...

Console.WriteLine("I was here.");

if (edi is not null)
    edi.Throw();

コード例のファイルが存在しない場合は、次の出力が生成されます。

I was here.
Unhandled exception. System.IO.FileNotFoundException: Could not find file 'C:\temp\file.txt'.
File name: 'C:\temp\file.txt'
   at Microsoft.Win32.SafeHandles.SafeFileHandle.CreateFile(String fullPath, FileMode mode, FileAccess access, FileShare share, FileOptions options)
   at Microsoft.Win32.SafeHandles.SafeFileHandle.Open(String fullPath, FileMode mode, FileAccess access, FileShare share, FileOptions options, Int64 preallocationSize, Nullable`1 unixCreateMode)
   at System.IO.Strategies.OSFileStreamStrategy..ctor(String path, FileMode mode, FileAccess access, FileShare share, FileOptions options, Int64 preallocationSize, Nullable`1 unixCreateMode)
   at System.IO.Strategies.FileStreamHelpers.ChooseStrategyCore(String path, FileMode mode, FileAccess access, FileShare share, FileOptions options, Int64 preallocationSize, Nullable`1 unixCreateMode)
   at System.IO.StreamReader.ValidateArgsAndOpenPath(String path, Encoding encoding, Int32 bufferSize)
   at System.IO.File.ReadAllText(String path, Encoding encoding)
   at Example.ProcessFile.Main() in C:\repos\ConsoleApp1\Program.cs:line 12
--- End of stack trace from previous location ---
   at Example.ProcessFile.Main() in C:\repos\ConsoleApp1\Program.cs:line 24

例外のスロー

次のベスト プラクティスは、例外をスローする方法に関するものです。

定義済みの例外の種類を使用する

定義済みの例外クラスが適用されない場合にのみ、新しい例外クラスを導入します。 例えば次が挙げられます。

  • オブジェクトの現在の状態を考慮してプロパティセットまたはメソッド呼び出しが適切でない場合は、InvalidOperationException例外をスローします。
  • 無効なパラメーターが渡された場合は、 ArgumentException 例外または ArgumentExceptionから派生する定義済みのクラスの 1 つをスローします。

可能な限り定義済みの例外の種類を使用することをお勧めしますが、AccessViolationExceptionIndexOutOfRangeExceptionNullReferenceExceptionなど、一部のStackOverflowException例外の種類を発生させてはいけません。 詳細については、「 CA2201: 予約済み例外の種類を発生させないでください」を参照してください。

例外ビルダー メソッドを使用する

クラスでは、その実装内のさまざまな場所から同じ例外がスローされるのが一般的です。 過剰なコードを回避するには、例外を作成して返すヘルパー メソッドを作成します。 例えば次が挙げられます。

class FileReader
{
    private readonly string _fileName;

    public FileReader(string path)
    {
        _fileName = path;
    }

    public byte[] Read(int bytes)
    {
        byte[] results = FileUtils.ReadFromFile(_fileName, bytes) ?? throw NewFileIOException();
        return results;
    }

    static FileReaderException NewFileIOException()
    {
        string description = "My NewFileIOException Description";

        return new FileReaderException(description);
    }
}
Class FileReader
    Private fileName As String


    Public Sub New(path As String)
        fileName = path
    End Sub

    Public Function Read(bytes As Integer) As Byte()
        Dim results() As Byte = FileUtils.ReadFromFile(fileName, bytes)
        If results Is Nothing
            Throw NewFileIOException()
        End If
        Return results
    End Function

    Function NewFileIOException() As FileReaderException
        Dim description As String = "My NewFileIOException Description"

        Return New FileReaderException(description)
    End Function
End Class

一部の主要な .NET 例外の種類には、例外を割り当ててスローする静的 throw ヘルパー メソッドがあります。 対応する例外の種類を構築してスローするのではなく、次のメソッドを呼び出す必要があります。

ヒント

次のコード分析規則は、throw の静的な ヘルパーを利用できるコード内の場所を見つけるのに役立ちます。

非同期メソッドを実装する際には、キャンセルが要求されたかどうかを確認してから CancellationToken.ThrowIfCancellationRequested() を構築してスローするのではなく、まず OperationCanceledException を呼び出してください。 詳細については、 CA2250 を参照してください。

ローカライズされた文字列メッセージを含める

ユーザーに表示されるエラー メッセージは、例外クラスの名前ではなく、スローされた例外の Exception.Message プロパティから派生します。 通常は、Exception.Messagemessage引数にメッセージ文字列を渡すことによって、 プロパティに値を割り当てます。

ローカライズされたアプリケーションの場合は、アプリケーションがスローできるすべての例外に対してローカライズされたメッセージ文字列を指定する必要があります。 リソース ファイルを使用して、ローカライズされたエラー メッセージを提供します。 アプリケーションのローカライズとローカライズされた文字列の取得については、次の記事を参照してください。

適切な文法を使用する

明確な文を記述し、終了句読点を含めます。 Exception.Message プロパティに割り当てられた文字列内の各文は、ピリオドで終わる必要があります。 たとえば、"ログ テーブルがオーバーフローしました" は、正しい文法と句読点を使用します。

throw ステートメントを適切に配置する

スタック トレースが役立つ場所に throw ステートメントを配置します。 スタック トレースは、例外がスローされるステートメントから始まり、例外をキャッチする catch ステートメントで終了します。

finally 句で例外を発生させない

finally句で例外を発生させないでください。 詳細については、コード分析規則 CA2219 を参照してください。

予期しない場所から例外を発生させないでください

EqualsGetHashCodeToStringメソッド、静的コンストラクター、等値演算子などの一部のメソッドでは、例外をスローしないでください。 詳細については、コード分析規則 CA1065 を参照してください。

引数検証例外を同期的にスローする

タスクを返すメソッドでは、引数を検証し、メソッドの非同期部分を入力する前に、 ArgumentExceptionArgumentNullExceptionなどの対応する例外をスローする必要があります。 メソッドの非同期部分でスローされる例外は、返されたタスクに格納され、たとえば、タスクが待機されるまで出現しません。 詳細については、「 タスクを返すメソッドの例外」を参照してください。

カスタム例外の種類

次のベスト プラクティスは、カスタム例外の種類に関します。

例外クラス名はExceptionで終了させる

カスタム例外が必要な場合は、適切な名前を付け、 Exception クラスから派生させます。 例えば次が挙げられます。

public class MyFileNotFoundException : Exception
{
}
Public Class MyFileNotFoundException
    Inherits Exception
End Class

3 つのコンストラクターを含める

独自の例外クラスを作成するときは、少なくとも 3 つの一般的なコンストラクター (パラメーターなしのコンストラクター、文字列メッセージを受け取るコンストラクター、および文字列メッセージと内部例外を受け取るコンストラクター) を使用します。

例については、「 方法: ユーザー定義例外を作成する」を参照してください。

必要に応じてさらにプロパティを指定する

追加情報が役に立つプログラムによるシナリオがある場合にのみ、例外のプロパティを追加します (カスタム メッセージ文字列に加えて)。 たとえば、 FileNotFoundExceptionFileName プロパティを提供します。

こちらも参照ください