C# 6.0で導入された「例外フィルタ(Exception Filters)」を使用すると、catchブロックに入る条件を詳細に指定できます。これにより、単に例外の型だけでなく、例外オブジェクトが持つプロパティの値や、外部の状態に基づいて、捕捉するかどうかを決定することが可能になります。
従来の「一旦キャッチしてからifで判定し、対象外ならthrowで再スローする」という方法と比較して、コードの可読性が向上するだけでなく、デバッグ時のスタックトレース情報がより正確に保たれるという利点があります。
ここでは、データベース操作における「一意制約違反(重複エラー)」と「接続エラー」を、同じ例外型でありながら別の処理として書き分けるシナリオを例に解説します。
例外フィルタの基本構文
catch句の後ろにwhenキーワードを記述し、その後にbool値を返す条件式を書きます。
catch (ExceptionType ex) when (条件式)
{
// 条件式が true の場合のみ実行される
}
実践的なコード例:エラーコードによる処理の分岐
以下のコードは、データベース操作を模したクラスで、エラーコードに応じて異なる例外処理を行う例です。SqlExceptionなどのデータベース例外は、エラーの種類(主キー重複、接続タイムアウトなど)を数値のコードで持っていることが一般的です。
※ここでは標準ライブラリだけで動作確認ができるよう、独自のDatabaseExceptionクラスを定義して解説します。
using System;
namespace DatabaseApp
{
// データベースエラーを模したカスタム例外
public class DatabaseException : Exception
{
public int ErrorCode { get; }
public DatabaseException(string message, int errorCode) : base(message)
{
ErrorCode = errorCode;
}
}
class Program
{
// エラーコード定数
private const int ErrorDuplicateKey = 2601; // 重複エラー
private const int ErrorConnectionFailed = 53; // 接続エラー
static void Main()
{
try
{
// ユーザー登録処理を実行(ここで例外が発生すると仮定)
// 引数を変えることで挙動を確認できます:
// 1: 重複エラー, 2: 接続エラー, 3: その他のエラー
RegisterUser("user01", 1);
}
// 1. エラーコードが「重複エラー(2601)」の場合のみ捕捉
catch (DatabaseException ex) when (ex.ErrorCode == ErrorDuplicateKey)
{
Console.WriteLine("【処理済み】ユーザーIDが重複しています。別のIDを指定してください。");
Console.WriteLine($"詳細: {ex.Message}");
}
// 2. エラーコードが「接続エラー(53)」の場合のみ捕捉
catch (DatabaseException ex) when (ex.ErrorCode == ErrorConnectionFailed)
{
Console.WriteLine("【処理済み】データベースに接続できません。再試行してください。");
Console.WriteLine($"詳細: {ex.Message}");
}
// 3. 上記のフィルタに該当しない DatabaseException を捕捉
catch (DatabaseException ex)
{
Console.WriteLine("【汎用処理】データベースエラーが発生しました。");
Console.WriteLine($"コード: {ex.ErrorCode}, メッセージ: {ex.Message}");
}
// 4. その他の予期せぬ例外
catch (Exception ex)
{
Console.WriteLine($"予期せぬエラー: {ex.Message}");
}
}
// データベース登録をシミュレートするメソッド
static void RegisterUser(string userId, int scenarioType)
{
Console.WriteLine($"ユーザー '{userId}' を登録します...");
switch (scenarioType)
{
case 1:
throw new DatabaseException("Primary key violation", ErrorDuplicateKey);
case 2:
throw new DatabaseException("Network path not found", ErrorConnectionFailed);
default:
throw new DatabaseException("Unknown database error", 9999);
}
}
}
}
実行結果(scenarioType = 1 の場合)
Plaintext
ユーザー 'user01' を登録します...
【処理済み】ユーザーIDが重複しています。別のIDを指定してください。
詳細: Primary key violation
技術的なポイントとメリット
1. スタックトレースの保存(スタックアンワインドの防止)
when句を使用する最大の技術的メリットは、**「条件に合致しない場合、スタックが巻き戻されない(Unwindされない)」**点です。
もしcatchブロック内でifを使って分岐し、対象外の場合にthrow;で再スローすると、一度そのcatchブロックまでスタックが巻き戻ってしまいます。これにより、デバッガで調査する際、本来の発生箇所ではなくthrowした箇所がハイライトされたり、コンテキスト情報の一部が失われたりする可能性があります。 例外フィルタを使用すれば、条件に合致しない例外はそもそもキャッチされなかったものとして扱われ、スタック情報はそのまま維持されます。
2. ロギングへの応用
例外フィルタは、例外をキャッチせずにログだけ出力するというトリッキーな使い方も可能です。
catch (Exception ex) when (LogException(ex))
{
// ここには到達しない
}
// 常にfalseを返すメソッド
static bool LogException(Exception ex)
{
Console.WriteLine($"ログ出力: {ex.Message}");
return false; // falseを返すことでcatchブロックには入らない
}
このパターンを使用すると、プログラムの実行を中断・変更することなく、通過する例外を記録できます(ただし、乱用すると制御フローが分かりにくくなるため注意が必要です)。
3. 多重catchの可読性向上
同じ例外型に対して、エラーコードやメッセージの内容によって異なるリカバリ処理を実装したい場合、when句を使うことでそれぞれの処理を独立したcatchブロックとして記述できます。これにより、巨大なif-else文がcatchブロック内に作られることを防げます。
まとめ
例外フィルタ(when句)は、特定のエラーコードや条件に基づいて例外ハンドリングを細分化するための強力な機能です。特にデータベース操作やHTTP通信など、一つの例外型の中に多種多様なエラー要因が含まれるケースにおいて、コードの堅牢性と可読性を高めるために積極的に採用すべき構文です。
