例外の推奨事項

適切にデザインされたアプリケーションは、アプリケーションのクラッシュを防ぐために、例外やエラーを処理します。 ここでは、例外の処理と作成のためのベスト プラクティスについて説明します。

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)
{
    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 (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

選択するメソッドは、予期されるイベント発生頻度によって決まります。

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

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

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

クラスは、例外をトリガーする呼び出しを行うことを回避できるようにするメソッドとプロパティを提供できます。 たとえば、FileStream クラスには、ファイルの終端に到達したかどうかを判別するために役立つメソッドが用意されています。 これらのメソッドは、ファイルの終わりを越えて読み取った場合にスローされる例外を回避するために使用できます。 次の例では、例外をトリガーすることなく、ファイルの終わりまで読み取る方法を示します。

class FileRead
{
public:
    void ReadAll(FileStream^ fileToRead)
    {
        // This if statement is optional
        // as it is very unlikely that
        // the stream would ever be null.
        if (fileToRead == nullptr)
        {
            throw gcnew System::ArgumentNullException();
        }

        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 void ReadAll(FileStream fileToRead)
    {
        // This if statement is optional
        // as it is very unlikely that
        // the stream would ever be null.
        if (fileToRead == null)
        {
            throw new ArgumentNullException();
        }

        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> または既定値をエラー インジケーターとして使用するかどうかをアプリに関して検討します。 Nullable<Guid> を使用すると、defaultGuid.Empty ではなく null になります。 Nullable<T> を追加すると、値があるときとないときがはっきりすることがあります。 Nullable<T> を追加すると、不要な確認事項が増え、潜在的なエラーの原因にしかならないこともあります。

エラー コードを返す代わりに、例外をスローする

呼び出しコードではリターン コードを確認しないので、例外によって、エラーを見過ごさないようにします。

事前定義済みの .NET の例外の種類を使用する

事前定義の例外クラスが適用されない場合に限り、新しい例外クラスを導入します。 次に例を示します。

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

例外クラス名の末尾に Exception という単語を付加する

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

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

カスタム例外クラスに 3 つのコンストラクターを含める

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

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

コードがリモートで実行されるときに、例外データが利用できるようにする

ユーザー定義例外を作成するときには、確実にリモートで実行されるコードで例外のメタデータを使用できるようにします。

たとえば、アプリ ドメインをサポートする .NET 実装では、アプリ ドメイン間で例外が発生する可能性があります。 アプリ ドメイン A が、例外をスローするコードを実行するアプリ ドメイン B を作成するとします。 アプリ ドメイン A で例外を適切にキャッチして処理するには、アプリ ドメイン B によりスローされた例外が含まれているアセンブリを検出できる必要があります。アプリ ドメイン B で、アプリ ドメイン A のアプリケーション ベースではなく、自身のアプリケーション ベースの下のアセンブリに含まれている例外をスローする場合、アプリ ドメイン A では、例外を検出できなくなり、共通言語ランタイムで FileNotFoundException 例外がスローされます。 このような状況を回避するには、例外情報が含まれているアセンブリを次のいずれかの方法で配置します。

  • 2 つのアプリ ドメインが共有する共通アプリケーション ベースにアセンブリを配置する。
  • ドメインで共通アプリケーション ベースを共有していない場合には、例外情報が含まれているアセンブリに厳密な名前で署名し、グローバル アセンブリ キャッシュにこのアセンブリを配置する。

文法的に正しいエラー メッセージを使用する

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

すべての例外に、ローカライズした文字列メッセージを含める

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

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

カスタム例外で、必要に応じて追加のプロパティを提供する

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

スタック トレースが役に立つように throw ステートメントを配置する

例外がスローされたステートメントからスタック トレースが開始され、例外をキャッチした catch ステートメントでトレースが終了します。

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

クラスでは、その実装内のさまざまな場所から同じ例外がスローされるのが一般的です。 コードが長くなることを防ぐため、例外を作成して返すヘルパー メソッドを使用します。 次に例を示します。

ref class FileReader
{
private:
    String^ fileName;

public:
    FileReader(String^ path)
    {
        fileName = path;
    }

    array<Byte>^ Read(int bytes)
    {
        array<Byte>^ results = FileUtils::ReadFromFile(fileName, bytes);
        if (results == nullptr)
        {
            throw NewFileIOException();
        }
        return results;
    }

    FileReaderException^ NewFileIOException()
    {
        String^ description = "My NewFileIOException Description";

        return gcnew FileReaderException(description);
    }
};
class FileReader
{
    private string fileName;

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

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

    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

場合によっては、例外のコンストラクターを使用して例外を作成する方が適切な場合もあります。 ArgumentException などのグローバル例外クラスはその一例です。

例外に起因してメソッドが完了しないとき、状態を復元する

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

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

例外をキャプチャして後で再スローする

例外をキャプチャし、後で再スローできるようにその呼び出し履歴を保持するには、System.Runtime.ExceptionServices.ExceptionDispatchInfo クラスを使用します。 このクラスには、特に次のようなメソッドとプロパティが用意されています。

次の例は、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

関連項目