基礎から学ぶC#入門 90日コース | 実践的なC#プログラミング - Day 52:独自例外とエラー設計

C# 90日で身につけるC#
スポンサーリンク
スポンサーリンク

Day 52 のゴールと全体像

Day 52 では、昨日の「例外処理」の一歩先に進んで、 「独自例外」と「エラー設計」という、少し大人なテーマに踏み込んでいきます。

キーワードは次の3つです。

  • エラーの種類
  • 例外を使うべき場面
  • 入力チェック

「エラーが起きたらとりあえず try-catch で囲む」から卒業して、 どんなエラーを、どのレイヤーで、どう扱うべきかを考えられるようになることが、今日のゴールです。

エラーの種類を整理してみる

すべて「エラー」でひとくくりにしない

まずは、「エラー」と一言で言ってしまいがちなものを、 少し丁寧に分類してみます。

ざっくり分けると、次のような種類があります。

  1. ユーザー入力のエラー
    • 数字を入れるべきところに文字列が入っている
    • 必須項目が空欄になっている
    • 範囲外の値が指定されている
  2. 環境・外部要因のエラー
    • ファイルが存在しない
    • ネットワークに接続できない
    • データベースが落ちている
  3. プログラムのバグによるエラー
    • null を参照してしまった
    • 想定していない状態でメソッドが呼ばれた
    • ロジックのミスで矛盾した状態になっている

このうち、

  • ユーザー入力のエラーは「想定内の失敗」
  • 環境・外部要因のエラーは「起こりうるが制御しづらい失敗」
  • バグによるエラーは「本来あってはならない失敗」

という性質を持っています。

この性質の違いが、 「例外を使うべき場面」と「使うべきでない場面」を分けるポイントになってきます。

例外を使うべき場面・使うべきでない場面

例外は「本当に例外的な状況」に使う

昨日学んだように、例外は「普通じゃない事態」を表す仕組みです。 では、どんなときに例外を使うべきなのでしょうか。

例外を使うべき場面

  • プログラムの前提が破られたとき
    • ありえないはずの状態になっている
    • 呼び出し側が契約を守っていない(不正な引数など)
  • 外部リソースの利用に失敗したとき
    • ファイルがない
    • ネットワークが切れている
    • データベース接続に失敗した
  • 処理を続行してはいけないレベルの問題が起きたとき
    • 計算結果が明らかにおかしい
    • セキュリティ上、処理を止めるべき状況

例外を使うべきでない場面

  • ユーザー入力のちょっとした間違い
    • 数字を入れるべきところに文字列が入っている
    • 必須項目が空欄になっている → これは「よくあること」であり、例外というより「バリデーションエラー」です。
  • 通常の分岐で十分に扱えるケース
    • 値が範囲内かどうかをチェックして、範囲外ならメッセージを出す → if 文で十分に表現できるものに、わざわざ例外を使う必要はありません。

例外は「制御フローをジャンプさせる」強力な仕組みなので、 乱用するとコードが読みにくくなり、テストもしづらくなります。

独自例外クラスを作ってみる

「意味のある名前」を持った例外を定義する

C# にはたくさんの標準例外クラスがありますが、 アプリケーション固有のエラーを表現したいときには、 独自の例外クラスを定義することがあります。

たとえば、ユーザー登録システムで、 「ユーザー名がすでに使われている」というエラーを表現したいとしましょう。

using System;

// 独自の例外クラス(ユーザー名の重複を表す)
class DuplicateUserNameException : Exception
{
    public string UserName { get; }

    public DuplicateUserNameException(string userName)
        : base($"ユーザー名 '{userName}' はすでに使用されています。")
    {
        UserName = userName;
    }
}
C#

ポイントはここです。

  • Exception クラスを継承して独自例外を作っています。
  • コンストラクタでユーザー名を受け取り、 メッセージにもそのユーザー名を含めています。
  • UserName プロパティを持たせることで、 キャッチした側が「どのユーザー名が問題だったのか」を知ることができます。

独自例外を投げる側のコード

この例外を使って、ユーザー登録処理を書いてみます。

using System;
using System.Collections.Generic;

class UserService
{
    private readonly HashSet<string> userNames = new HashSet<string>();

    // ユーザーを登録するメソッド
    public void Register(string userName)
    {
        if (string.IsNullOrWhiteSpace(userName))
        {
            // 入力チェック(これは例外ではなく通常のバリデーション)
            Console.WriteLine("ユーザー名は必須です。");
            return;
        }

        if (userNames.Contains(userName))
        {
            // ここで独自例外を投げる
            throw new DuplicateUserNameException(userName);
        }

        userNames.Add(userName);
        Console.WriteLine($"ユーザー '{userName}' を登録しました。");
    }
}
C#

独自例外をキャッチする側のコード

using System;

class Program
{
    static void Main(string[] args)
    {
        var service = new UserService();

        try
        {
            service.Register("taro");
            service.Register("hanako");
            service.Register("taro"); // ここで重複が発生する
        }
        catch (DuplicateUserNameException ex)
        {
            Console.WriteLine("ユーザー登録に失敗しました。");
            Console.WriteLine($"原因: ユーザー名 '{ex.UserName}' はすでに使われています。");
            Console.WriteLine($"詳細メッセージ: {ex.Message}");
        }

        Console.WriteLine("処理を終了します。");
        Console.ReadLine();
    }
}
C#

ここでの大事なポイント:

  • 独自例外を使うことで、「何が問題だったのか」を明確な型として表現できます。
  • DuplicateUserNameException という名前だけで、 「ユーザー名の重複に関するエラーだな」とすぐにわかります。

入力チェックと例外の役割分担

すべてを例外にしない勇気

入力チェック(バリデーション)は、 例外とは少し違う性質を持っています。

  • 入力チェックは「よくある失敗」
  • 例外は「本来起きてほしくない、あるいは重大な失敗」

この違いを意識しておくことが大切です。

具体例:年齢入力のチェック

ユーザーに年齢を入力してもらう場面を考えてみます。

using System;

class AgeInputExample
{
    public static int ReadAge()
    {
        Console.WriteLine("年齢を入力してください。");

        string text = Console.ReadLine();

        // 1. 数字かどうかをチェック(これは例外ではなくバリデーション)
        if (!int.TryParse(text, out int age))
        {
            Console.WriteLine("数字を入力してください。");
            return -1; // エラーを示す値(本来は別の方法で返すことも多い)
        }

        // 2. 範囲チェック(これもバリデーション)
        if (age < 0 || age > 120)
        {
            Console.WriteLine("年齢は 0〜120 の範囲で入力してください。");
            return -1;
        }

        return age;
    }
}
C#

ここでは、

  • int.Parse ではなく int.TryParse を使うことで、 例外ではなく「戻り値」で失敗を表現しています。
  • 範囲チェックも if 文で行い、 ユーザーにメッセージを返しています。

このように、「よくある入力ミス」は例外ではなくバリデーションで扱う方が自然です。

エラー設計の基本的な考え方

「どこで検知して、どこで伝えて、どこで止めるか」を決める

エラー設計を考えるときに、 意識しておきたいポイントをいくつか挙げてみます。

  1. エラーの種類を分ける
    • 入力ミスなのか
    • 外部要因なのか
    • バグなのか
  2. どのレイヤーで扱うかを決める
    • UI 層(画面・コンソール)
    • ビジネスロジック層
    • インフラ層(ファイル・DB・ネットワーク)
  3. 例外とバリデーションの役割分担を決める
    • バリデーション:ユーザー入力のチェック
    • 例外:契約違反・外部リソースの失敗・重大な異常
  4. 独自例外を使うかどうかを判断する
    • アプリ固有のエラーを、意味のある名前で表現したいときに使う

小さなテンプレートとしての「サービス+例外+バリデーション」

最後に、エラー設計の基本形として使える、 小さなテンプレートを示しておきます。

using System;

class InvalidUserInputException : Exception
{
    public InvalidUserInputException(string message)
        : base(message)
    {
    }
}

class UserRegistrationService
{
    public void Register(string userName, int age)
    {
        // 入力チェック(バリデーション)
        if (string.IsNullOrWhiteSpace(userName))
        {
            throw new InvalidUserInputException("ユーザー名は必須です。");
        }

        if (age < 0 || age > 120)
        {
            throw new InvalidUserInputException("年齢は 0〜120 の範囲で入力してください。");
        }

        // ここから先は「ビジネスロジック」
        Console.WriteLine($"ユーザー '{userName}'({age}歳)を登録しました。");
    }
}

class Program
{
    static void Main(string[] args)
    {
        var service = new UserRegistrationService();

        try
        {
            Console.WriteLine("ユーザー名を入力してください。");
            string name = Console.ReadLine();

            Console.WriteLine("年齢を入力してください。");
            string ageText = Console.ReadLine();

            if (!int.TryParse(ageText, out int age))
            {
                Console.WriteLine("年齢は数字で入力してください。");
                return;
            }

            service.Register(name, age);
        }
        catch (InvalidUserInputException ex)
        {
            Console.WriteLine("入力内容に問題があります。");
            Console.WriteLine($"詳細: {ex.Message}");
        }
        catch (Exception ex)
        {
            Console.WriteLine("予期しないエラーが発生しました。");
            Console.WriteLine($"詳細: {ex.Message}");
        }

        Console.WriteLine("処理を終了します。");
        Console.ReadLine();
    }
}
C#

このコードには、

  • 入力チェック(TryParse と範囲チェック)
  • 独自例外(InvalidUserInputException
  • 例外の種類ごとの catch

という、Day 52 のテーマがコンパクトに詰まっています。

Day 52 のまとめ

Day 52 では、

  • エラーの種類を整理し、 「入力ミス」「外部要因」「バグ」という性質の違いを意識しました。
  • 例外を使うべき場面と、 使うべきでない場面(特に入力チェック)を区別しました。
  • 独自例外クラスを定義し、 アプリ固有のエラーを「意味のある名前」で表現する方法を学びました。
  • 入力チェックと例外の役割分担を、 実際のコード例を通して体感しました。

エラー設計は、 「とりあえず動けばいい」という段階を超えて、

失敗したときに、 ユーザーにとっても、開発者にとっても、 わかりやすく、扱いやすい形で情報を届ける

ための大事な技術です。

ぜひ、

  • 独自例外に名前をつけてみる
  • 入力チェックを if で書くか、例外で表現するかを意識して選ぶ
  • どのレイヤーでどのエラーを扱うかを、頭の中で整理してみる

といった小さな工夫を重ねながら、 「エラーと仲良く付き合えるコード」を少しずつ育てていってください。

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