C#にはArgumentExceptionやInvalidOperationExceptionなど、多くの標準例外クラスが用意されています。しかし、アプリケーション固有のビジネスロジックエラー(例:在庫不足、アカウントロック、残高不足など)を表現する場合、標準の例外クラスだけでは意味が曖昧になることがあります。
このような場合、Exceptionクラスを継承した「独自例外(カスタム例外)」を定義することで、エラーの意味を明確にし、呼び出し元でのハンドリングを容易にすることができます。ここでは、銀行口座の引き出し処理を題材に、残高不足を表す独自例外の実装方法を解説します。
独自例外クラスの定義ルール
.NETの設計ガイドラインでは、例外クラスを作成する際に以下のルールに従うことが推奨されています。
- 継承元:
System.Exceptionクラスを継承する。 - 名称: クラス名の末尾には必ず
Exceptionを付ける(例:InsufficientFundsException)。 - コンストラクタ: 少なくとも以下の3つの標準コンストラクタを実装する。
- 引数なしのデフォルトコンストラクタ
- エラーメッセージを受け取るコンストラクタ
- エラーメッセージと内部例外(InnerException)を受け取るコンストラクタ
実践的なコード例:残高不足例外の実装
以下のコードは、銀行口座の引き出し操作において、残高が不足している場合にスローされるInsufficientFundsExceptionを定義し、利用する例です。
using System;
namespace BankingSystem
{
// 1. 独自例外クラスの定義
// ビジネスロジック上の「残高不足」を表すため、Exceptionを継承します。
public class InsufficientFundsException : Exception
{
// --- 推奨される3つの標準コンストラクタ ---
// (1) デフォルトコンストラクタ
public InsufficientFundsException()
{
}
// (2) エラーメッセージを受け取るコンストラクタ
public InsufficientFundsException(string message)
: base(message)
{
}
// (3) メッセージと内部例外を受け取るコンストラクタ(例外のラップ用)
public InsufficientFundsException(string message, Exception innerException)
: base(message, innerException)
{
}
// (オプション) 追加のプロパティを持たせることも可能です
// 例: 不足している金額
public decimal DeficitAmount { get; set; }
}
// 口座クラス
public class BankAccount
{
public decimal Balance { get; private set; }
public BankAccount(decimal initialBalance)
{
Balance = initialBalance;
}
public void Withdraw(decimal amount)
{
if (amount > Balance)
{
decimal deficit = amount - Balance;
// 独自例外をインスタンス化してスロー
// 必要に応じて独自のプロパティ(DeficitAmount)にも値をセット
var ex = new InsufficientFundsException($"残高が不足しています。現在の残高: {Balance:C}, 要求額: {amount:C}");
ex.DeficitAmount = deficit;
throw ex;
}
Balance -= amount;
Console.WriteLine($"出金完了: {amount:C} (残高: {Balance:C})");
}
}
class Program
{
static void Main()
{
var account = new BankAccount(1000); // 残高1,000円
try
{
// 5,000円を引き出そうとする(残高不足)
account.Withdraw(5000);
}
// 独自例外を明示的にキャッチ
catch (InsufficientFundsException ex)
{
Console.WriteLine("=== エラー: 取引失敗 ===");
Console.WriteLine(ex.Message);
Console.WriteLine($"不足金額: {ex.DeficitAmount:C}");
}
// その他の予期せぬ例外
catch (Exception ex)
{
Console.WriteLine($"システムエラー: {ex.Message}");
}
}
}
}
実行結果
=== エラー: 取引失敗 ===
残高が不足しています。現在の残高: ¥1,000, 要求額: ¥5,000
不足金額: ¥4,000
技術的なポイント
1. 標準コンストラクタの重要性
独自例外を作成する際は、親クラスであるExceptionが持つ機能を損なわないようにする必要があります。特に (string message, Exception inner) コンストラクタは重要です。これがないと、下位レイヤーで発生した例外をこの独自例外でラップする際、InnerExceptionチェーンが途切れてしまい、根本原因の追跡が困難になります。
2. 独自プロパティの活用
標準のExceptionクラスを使うのではなく独自例外を作る大きなメリットの一つは、独自のプロパティを追加できる点です。 上記の例では DeficitAmount(不足額)プロパティを追加しています。これにより、catchブロック側ではエラーメッセージの文字列解析(パース)を行うことなく、不足額に応じたプログラム的な処理(例:不足分を別口座から補填するロジックなど)を安全に記述できます。
3. 使用すべきタイミング
すべてのエラーに対して独自例外を作る必要はありません。
- 標準例外で十分な場合: 引数がnullなら
ArgumentNullException、操作が無効ならInvalidOperationExceptionを使用します。 - 独自例外が必要な場合: 「特定の業務ルール違反」であり、呼び出し元でそのエラーを特別扱いしてリカバリ処理を行いたい場合に定義します。
まとめ
独自例外を定義することで、コードの意図(セマンティクス)が明確になり、エラー処理の構造が整理されます。推奨される3つのコンストラクタを必ず実装し、必要に応じて独自のデータを持たせることで、堅牢で保守性の高いエラーハンドリングを実現してください。
