C# 例外フィルター

C#

C# 6.0 以降では、catchブロックに例外フィルター(when句)を追加することができるようになりました。

C#の例外フィルタは、特定の条件が満たされた場合にのみ例外をキャッチする強力なメカニズムを提供します。 これにより、例外を再スローしたり、キャッチブロック内で追加チェックを行ったりする必要がなくなり、より正確な例外処理が可能になります。

例外フィルターのメリットをまとめると次のようになります。

  • より詳細な例外処理: 例外の種類だけでなく、その例外が持つデータやプログラムの状態に基づいて、きめ細やかな処理が可能になります。
  • コードの可読性向上: 複数のif文で条件分岐させるよりも、when句を使うことで意図が明確になります。
  • 例外の再スローを回避: 特定の条件で例外をキャッチしない場合、catchブロック内で再スローする必要がなくなります。これにより、スタックトレースが保持されやすくなります。

フィルタ条件は、キャッチブロックのためにスタックが巻き戻される前に評価されます。つまり、フィルターがfalseを返した場合、スタックは巻き戻されず、例外はあたかもcatchブロックが存在しなかったかのようにコールスタックを進みます。 これは、巻き戻す前にアプリケーションの状態を検査することが重要なシナリオでは有益です。

基本構文は次のようなものです。

C#
try
{
    // ...
}
catch (ExceptionType ex) when (フィルター条件)
{
    // フィルタ条件がtrueの場合のみ実行されるコード
}

例外プロパティによるフィルタリング

設定ファイルを解析しているときにInvalidOperationExceptionがスローされるシナリオを想定します。 例外の Data プロパティに特定のエラーコードがある場合のみ、例外を処理したいとします。

C#
using System;
using System.Collections.Generic;

public class ConfigurationParser
{
    public void ParseConfiguration(Dictionary<string, string> configData)
    {
        try
        {
            if (!configData.ContainsKey("ApplicationName"))
            {
                var ex = 
                new InvalidOperationException(
                  "configuration に ApplicationName がありません。");
                ex.Data["ErrorCode"] = 1001;
                throw ex;
            }

            if (!configData.ContainsKey("Version"))
            {
                var ex = new InvalidOperationException("Version 情報がありません。");
                ex.Data["ErrorCode"] = 1002;
                throw ex;
            }

        }
        catch (InvalidOperationException ex) 
          when (ex.Data.Contains("ErrorCode") && (int)ex.Data["ErrorCode"] == 1001)
        {
            ErrorLogger.WriteLine(
              $"Caught InvalidOperationException error code 1001: {ex.Message}");
        }
        catch (InvalidOperationException ex) 
        // General InvalidOperationException catch
        {
            ErrorLogger.WriteLine($"InvalidOperationException: {ex.Message}");
        }
        catch (Exception ex)
        {
            Console.WriteLine($"Caught a general Exception: {ex.Message}");
        }
    }

}

アプリケーションの状態によるフィルター

特定の機能フラグが有効な場合、あるいは特定のプロセス状態がアクティブな場合にのみ特別な例外処理をしたい場合を想定します。例外がリワインド(巻き戻し)される前のフラグなどのアプリケーションの状態を調べることができます。

C#
    public class ToggleSwitch
    {
        public static bool On { get; set; } = false;
        // ...
    }

    public class DataProcessor
    {
        public void ProcessData(int value)
        {
            try
            {
                if (value < 0)
                {
                    throw new ArgumentOutOfRangeException(
                                nameof(value), "負の値はとれません");
                }
                Console.WriteLine($"正しいプロセスを実行: {value}");
            }
            catch (ArgumentOutOfRangeException ex) when (ToggleSwitch.On)
            {
                // ToggleSwitch が On の時だけ catch します。
                // Off の場合にはスルーされます
                Console.WriteLine($"Toggleが On の場合の特別なエラーハンドリング: {ex.Message}");
            }
            catch (ArgumentOutOfRangeException ex)
            {
                // 通常のArgumentOutOfRangeExceptionのハンドリング
                Console.WriteLine($"一般的な エラーハンドリング: {ex.Message}");
            }
            catch (Exception ex)
            {
                Console.WriteLine($"予期しない例外: {ex.Message}");
            }
        }

        public static void Main(string[] args)
        {
            var processor = new DataProcessor();

            // シナリオ 1: トグルが On
            ToggleSwitch.On = false;
            processor.ProcessData(-5);

            // シナリオ 2: トグルが Off
            ToggleSwitch.On = true;
            processor.ProcessData(-10);

            // シナリオ 3: 正しい入力
            processor.ProcessData(20);
        }
    }

シナリオ1では、条件を満たさないため最初のブロックでは捕捉されず、次のキャッチブロックに進みます。
シナリオ2では、条件を満たすので、最初のcatchブロックが実行されます。

複合フィルター

同じ例外タイプに対して異なるフィルタを持つ複数のcatchブロックを持つことができるので、効果的に一連の条件付きハンドラを作成することができます。 catch ブロックは上から順番に評価されるので、順序は重要です。

C#
public void ProcessPayment(decimal amount)
{
    try
    {
        if (amount < 0)
        {
            throw new ArgumentException("amountは負の値をとれません。");
        }
        if (amount > 1000)
        {
            var ex = new InvalidOperationException("amountが限度を超えています。");
            ex.Data["ErrorCode"] = "LIMIT_EXCEEDED";
            throw ex;
        }
        if (amount == 0)
        {
            var ex = new InvalidOperationException("amountは0にできません。");
            ex.Data["ErrorCode"] = "ZERO_AMOUNT";
            throw ex;
        }
    }
    catch (InvalidOperationException ex) 
      when (ex.Data.Contains("ErrorCode") && 
        ex.Data["ErrorCode"]!.ToString() == "LIMIT_EXCEEDED")
    {
        // ...
    }
    catch (InvalidOperationException ex) 
      when (ex.Data.Contains("ErrorCode") && 
        ex.Data["ErrorCode"]!.ToString() == "ZERO_AMOUNT")
    {
        // ...
    }
    catch (InvalidOperationException ex)
    {
        // ...
    }
    catch (ArgumentException ex)
    {
        // ...
    }
    catch (Exception ex)
    {
        // ...
    }
}

重要な考慮事項

  • キャッチ・ブロックの順序:
    通常の catch ブロックと同様、順番は重要です。 より特殊な例外タイプやより特殊なフィルタは、一般的なものよりも前に書くのが一般的です。 フィルタを持つキャッチブロックがマッチした場合、そのキャッチブロックが実行され、後続のキャッチブロックはその例外について考慮されません。
  • フィルター条件のパフォーマンス:
    when節内のコードは軽量で副作用を避けるべきです。 それはスタックが巻き戻される前に実行されますが、大きなコードが実行されると、特に多くの例外がスローされフィルタリングされる場合のパフォーマンスに影響を与える可能性があります
  • フィルター内で再スローはできない:
    例外フィルター内で例外をスローすることはできません。 そうすると実行時エラーになります。
  • デバッグ:
    デバッガーはwhen句に達したとき、条件がfalseの場合catchブロックをスキップします。
  • catch 内の if 文を置き換える:
    例外フィルタは、catchブロックの中にif文を記述して、条件が満たされなかった場合に例外を再スローするよりも効率的です。 再スローにはスタックを巻き戻すというオーバーヘッドが発生するからです。

コメント

タイトルとURLをコピーしました