【C#】独自例外(カスタム例外)を適切に定義・実装する方法

C#にはArgumentExceptionInvalidOperationExceptionなど、多くの標準例外クラスが用意されています。しかし、アプリケーション固有のビジネスロジックエラー(例:在庫不足、アカウントロック、残高不足など)を表現する場合、標準の例外クラスだけでは意味が曖昧になることがあります。

このような場合、Exceptionクラスを継承した「独自例外(カスタム例外)」を定義することで、エラーの意味を明確にし、呼び出し元でのハンドリングを容易にすることができます。ここでは、銀行口座の引き出し処理を題材に、残高不足を表す独自例外の実装方法を解説します。


目次

独自例外クラスの定義ルール

.NETの設計ガイドラインでは、例外クラスを作成する際に以下のルールに従うことが推奨されています。

  1. 継承元: System.Exception クラスを継承する。
  2. 名称: クラス名の末尾には必ず Exception を付ける(例: InsufficientFundsException)。
  3. コンストラクタ: 少なくとも以下の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つのコンストラクタを必ず実装し、必要に応じて独自のデータを持たせることで、堅牢で保守性の高いエラーハンドリングを実現してください。

よかったらシェアしてね!
  • URLをコピーしました!
  • URLをコピーしました!

この記事を書いた人

私が勉強したこと、実践したこと、してることを書いているブログです。
主に資産運用について書いていたのですが、
最近はプログラミングに興味があるので、今はそればっかりです。

目次