例外の推奨事項

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

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

例外処理

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

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

例外を生成する可能性のあるコードの場合、およびアプリがその例外から回復できる場合は、そのコードの前後に try/catch ブロックを使用します catch ブロックでは、例外を常に最も強い派生型から最も弱い派生型の順序で並べ替えます。 (すべての例外は Exception クラスから派生します。他の派生例外は、基底例外クラスの catch 句の前にある catch 句では処理されません)。コードが例外から回復できない場合は、その例外はキャッチしないでください。 可能であれば、回復させる呼び出し履歴のずっと上でメソッドを有効にします。

using ステートメントまたは finally ブロックで割り当てられているリソースをクリーンアップします。 例外がスローされたとき、リソースを自動クリーンアップするには using ステートメントを選択します。 IDisposable を実装しないリソースをクリーンアップするには finally ブロックを使用します。 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

選択するアプローチは、イベントの予期される発生頻度によって異なります。

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

  • イベントが定期的に発生し、通常の実行の一部であると見なせる場合は、コード内でエラー条件をチェックします。 一般的なエラー条件をチェックするときに、例外を回避するためにより少ないコードが実行されます。

    Note

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

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

例外によるパフォーマンス コストが非常に大きい場合、一部の .NET ライブラリ メソッドを使用すると、別の形式のエラー処理を行うことができます。 たとえば、解析される値が大きすぎて Int32 で表すことができない場合、Int32.ParseOverflowException をスローします。 一方、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> を使用すると、defaultGuid.Empty ではなく null になります。 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 つをスローします。

Note

可能な場合は、事前定義された例外の種類を使用することをお勧めしますが、AccessViolationExceptionIndexOutOfRangeExceptionNullReferenceExceptionStackOverflowException などの一部の "予約された" の例外の種類が発生しないようにする必要があります。 詳細については、「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 ヘルパー メソッドなどがあります。 これらのメソッドは、対応する例外の種類を構築してスローする代わりに呼び出す必要があります。

ヒント

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

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

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

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

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

適切な文法を使用する

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

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

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

finally 句で例外が発生しないようにする

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

予期しない場所から例外が発生しないようにする

EqualsGetHashCodeToString などのメソッド、静的コンストラクター、等価演算子など、一部のメソッドから例外をスローしないようにする必要があります。 詳細については、コード分析規則 CA1065 を参照してください。

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

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

カスタム例外の種類

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

例外クラス名の末尾に Exception を付加する

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

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

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

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

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

必要に応じて追加のプロパティを提供する

プログラミングの点で追加情報が役立つ場合にだけ、(カスタム メッセージ文字列以外の) 例外の追加プロパティを含めてください。 たとえば、FileNotFoundException には FileName プロパティがあります。

関連項目