Day 52 のゴールと全体像
Day 52 では、昨日の「例外処理」の一歩先に進んで、 「独自例外」と「エラー設計」という、少し大人なテーマに踏み込んでいきます。
キーワードは次の3つです。
- エラーの種類
- 例外を使うべき場面
- 入力チェック
「エラーが起きたらとりあえず try-catch で囲む」から卒業して、 どんなエラーを、どのレイヤーで、どう扱うべきかを考えられるようになることが、今日のゴールです。
エラーの種類を整理してみる
すべて「エラー」でひとくくりにしない
まずは、「エラー」と一言で言ってしまいがちなものを、 少し丁寧に分類してみます。
ざっくり分けると、次のような種類があります。
- ユーザー入力のエラー
- 数字を入れるべきところに文字列が入っている
- 必須項目が空欄になっている
- 範囲外の値が指定されている
- 環境・外部要因のエラー
- ファイルが存在しない
- ネットワークに接続できない
- データベースが落ちている
- プログラムのバグによるエラー
- 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文で行い、 ユーザーにメッセージを返しています。
このように、「よくある入力ミス」は例外ではなくバリデーションで扱う方が自然です。
エラー設計の基本的な考え方
「どこで検知して、どこで伝えて、どこで止めるか」を決める
エラー設計を考えるときに、 意識しておきたいポイントをいくつか挙げてみます。
- エラーの種類を分ける
- 入力ミスなのか
- 外部要因なのか
- バグなのか
- どのレイヤーで扱うかを決める
- UI 層(画面・コンソール)
- ビジネスロジック層
- インフラ層(ファイル・DB・ネットワーク)
- 例外とバリデーションの役割分担を決める
- バリデーション:ユーザー入力のチェック
- 例外:契約違反・外部リソースの失敗・重大な異常
- 独自例外を使うかどうかを判断する
- アプリ固有のエラーを、意味のある名前で表現したいときに使う
小さなテンプレートとしての「サービス+例外+バリデーション」
最後に、エラー設計の基本形として使える、 小さなテンプレートを示しておきます。
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で書くか、例外で表現するかを意識して選ぶ - どのレイヤーでどのエラーを扱うかを、頭の中で整理してみる
といった小さな工夫を重ねながら、 「エラーと仲良く付き合えるコード」を少しずつ育てていってください。
