CodeGym /コース /C# SELF /コールスタックと独自例外の作り方

コールスタックと独自例外の作り方

C# SELF
レベル 13 , レッスン 3
使用可能

1. コールスタック(Call Stack)と仲良くなろう

前回のレクチャーでコールスタックをちょっとだけ触れたけど、今回はもうちょい深掘りするね。

想像してみて:ユーザーがアプリでボタンを押す。そのボタンがOnClick()(「ボタン押された」)メソッドを呼ぶ。そいつがLoadData()(データ読み込み)を呼んで、さらにReadFromFile()(ファイルから読む)を呼ぶ。

で、突然ReadFromFile()でエラー発生 ― ファイルが見つからない。誰が悪いの?

原因を探すために、プログラムは「足跡を逆にたどる」:ReadFromFile()LoadData()OnClick()。この道筋がコールスタックってやつ ― お皿の山みたいに、一番上のやつが最初に落ちる感じ。

プログラムはこの山を下りながら、適切なcatchを見つけるまで進むけど、その途中にある全部のfinallyもちゃんと実行する ― 片付けや解放を忘れないようにね。

プログラミングでコールスタックは「誰が誰を呼んだか」を覚えてるリストで、何かあった時にこの「呼び出しの道」を逆にたどって原因を探せるんだ。

どうなってるの?

プログラムが動いてる時、メソッド(または関数)を呼ぶたびにコールスタック(リスト)に新しい「行」が追加される。もし途中で例外が発生したら、.NETのExceptionクラスがこのスタック情報を保存してくれる:どこで、どんな順番でメソッドが呼ばれてエラーになったか、って感じ。

2. そもそもコールスタックって何のため?

コールスタックは、複雑なエラーのデバッグ("デバッグ")で一番の味方だよ。

  • 何が起きたかだけじゃなくて、どこで誰が原因かも分かる。
  • たまにスタックを見ると、「なんでこんな状態になったんだ?」ってビックリすることもある(特に誰かが間違った引数を渡した時とか)。

よくある話: 例えば、でっかいプロジェクトでメソッドが何重にも呼び合ってるとする。ある時突然NullReferenceExceptionが出て、「なんでここに来たの?」ってなる。スタックを開くと呼び出しのチェーンが全部見えて、どこから調べればいいか一気に分かりやすくなるよ。

例:

class MyClass 
{
    public void MethodA()     { MethodB(); }
    
    public void MethodB()     { MethodC(); }
    
    public void MethodC()     {
        throw new Exception("エラー!");
    }    

    public void Main() 
    {
        try 
        { 
            MethodA(); 
        }
        catch (Exception ex)
        {
            Console.WriteLine(ex.StackTrace);
        }        
    }
}

3. 独自例外の作り方

標準例外だけじゃ足りない時もある

.NETには標準例外が山ほどある(ArgumentNullExceptionInvalidOperationExceptionとか)が、時々それだけじゃ足りないこともあるよね:

  • アプリに独自の「ルール」がある場合:例えば、ユーザーは一度に10個以上商品を買えない、とか、ビジネスロジックでマイナスの金額はNGとか。
  • アプリのロジックエラーと「システム」エラーを分けたい時。

そんな時は「自分用」を作りたくなる ― せっかくだし!


using System;

// 独自例外:ユーザーが見つからない
public class UserNotFoundException : Exception
{
    // デフォルトコンストラクタ
    public UserNotFoundException() : base("ユーザーが見つかりません。") { }

    // メッセージ付きコンストラクタ
    public UserNotFoundException(string message) : base(message) { }

    // メッセージ+内部例外付きコンストラクタ
    public UserNotFoundException(string message, Exception inner) : base(message, inner) { }
}
独自例外 UserNotFoundExceptionの例

コンストラクタざっくり解説

  • パラメータなし ― 標準メッセージをセット
  • 好きなメッセージ付き ― 詳細を追加したい時に便利
  • 内部例外(inner)付き ― 「別のエラーの中で発生した」時に情報を失わないため

独自例外の使い方

例えば、タスク管理アプリでユーザー検索メソッドを作るとする:

using System;

public class UserService
{
    public string FindUserNameById(int userId)
    {
        // ユーザーを「探す」、見つからなければ例外を投げる
        if (userId != 42)
            throw new UserNotFoundException($"id {userId} のユーザーが見つかりません。");

        return "マキシム";
    }
}

メインプログラムでは:

// Mainで
var service = new UserService();
try
{
    string name = service.FindUserNameById(17);
    Console.WriteLine("ユーザー名: " + name);
}
catch (UserNotFoundException ex)
{
    Console.WriteLine("ユーザー検索の問題: " + ex.Message);
    // スタックトレースも ex.StackTrace で見れるよ
}

idが42以外を渡すと、こうなる:

ユーザー検索の問題: id 17 のユーザーが見つかりません。

4. 独自例外を作る理由

ログ分け&エラー分類

アプリにいろんなエラーがあって、それぞれ違う処理をしたい時。例えば、DBエラーは「致命的」としてログ、ユーザーエラーは赤字で表示、ネットワークエラーはリトライ…みたいに。例外の型でグループ分けするのが超便利。

OOPと継承

自分のドメイン用エラーの階層も作れる:

public class MyAppException : Exception { ... }
public class OrderException : MyAppException { ... }
public class ProductException : MyAppException { ... }
public class TooManyItemsInOrderException : OrderException { ... }

これでMyAppExceptionをcatchすれば全部自分のドメインエラーをまとめて処理できるし、「注文が多すぎ」だけ特別扱いしたい時は一番下のやつをcatchすればOK。

5. 独自例外を作る時に覚えておきたいこと

  • 例外を投げるためだけに投げない
    独自例外を作るのは、こんな時だけにしよう:
    • コードが分かりやすくなる時
    • 呼び出し元でcatchされる可能性がある時
    • 呼び出し元にもっと情報を渡したい時(フィールドやプロパティで)
  • 良い習慣:シリアライズ対応
    .NETの標準例外はシリアライズ(ネット越しに送るとか)に対応してる。シンプルなアプリならあまり使わないけど、「ガチ」なケースでは[Serializable]属性を付けてシリアライズ用コンストラクタも実装しよう(公式ドキュメント参照)。勉強用ならやらなくていいけど、仕事ならチームリーダーに聞いてみて :)
    シリアライズは、オブジェクトを保存や送信しやすい形式に変換すること(ファイルやネットワーク経由とか)。詳しくはまた後でやるよ。

6. コールスタックの注意点:混乱しやすいところ

コールスタックは例外が発生した「その場所」までの道しか表示しない。もし例外をcatchして新しい例外を投げる時に「内部例外」を指定しない(Exception(string, Exception inner)コンストラクタを使わない)と、元のエラー情報が消えちゃう。これを「スタック隠し」って呼ぶこともある。

ダメな例:

try
{
    // ここで何かエラー
}
catch (Exception ex)
{
    throw new Exception("不明なエラーが発生しました。"); // 前のスタックが消える!
}

良い例:

try
{
    // ここで何かエラー
}
catch (Exception ex)
{
    throw new Exception("不明なエラーが発生しました。", ex); // 元のスタックも残る!
}

こうすればStackTraceに最初のエラーの道筋も新しいメッセージも両方残るよ。

7. 実践アドバイス&よくあるミス

  • 普通のロジック制御に例外を使わない(例えば「ループ抜けるため」とか ― もっとスマートな方法がある!)
  • 必要な型だけcatchしよう ― いつもExceptionをcatchするのは微妙(本来キャッチしちゃいけないエラーまで飲み込むかも)。
  • 難しいエラーは必ずスタックトレースをログに残そう。バグ探しでめっちゃ助かる。
  • 内部例外を活用しよう ― 原因を見失わないために。
  • 独自例外の説明は分かりやすく ― 1年後にコードを読む人が「なんでこのエラー?」ってならないように。
2
タスク
C# SELF, レベル 13, レッスン 3
ロック未解除
例外処理時のスタックトレースの出力
例外処理時のスタックトレースの出力
2
タスク
C# SELF, レベル 13, レッスン 3
ロック未解除
カスタム例外の作成と処理
カスタム例外の作成と処理
コメント
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION