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には標準例外が山ほどある(ArgumentNullException、InvalidOperationExceptionとか)が、時々それだけじゃ足りないこともあるよね:
- アプリに独自の「ルール」がある場合:例えば、ユーザーは一度に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) { }
}
コンストラクタざっくり解説
- パラメータなし ― 標準メッセージをセット
- 好きなメッセージ付き ― 詳細を追加したい時に便利
- 内部例外(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年後にコードを読む人が「なんでこのエラー?」ってならないように。
GO TO FULL VERSION