例外処理について
例外処理とは
プログラムを実行していると、思わぬ問題が発生することがある。たとえば、存在しないファイルを読み込もうとしたり、0 で割り算をしようとした場合などがそれにあたる。
このような予期しない事態が起こったときに、プログラムが異常終了しないよう、安全に処理を続けたり終了したりするための仕組みが「例外処理」である。
例外とエラーについて
「エラー」とは、プログラムに何らかの問題が発生したことを表す広い概念である。一方、「例外(Exception)」は、プログラムの実行中に発生する特別なエラーであり、プログラムの処理の流れを中断させて、別の処理に切り替えるための信号のようなものである。
- エラー:コンパイルエラーや構文ミスなど、コードを書く段階で検出されるもの
- 例外:実行時に発生する予測困難な問題(例:ファイルが見つからない、0 での除算)
主な発生事例
以下は、例外が発生しやすい代表的なケースである:
- 存在しないファイルを開こうとしたとき(
FileNotFoundException
) - ネットワークエラーによって通信が失敗したとき(
HttpRequestException
) - 配列の範囲外にアクセスしようとしたとき(
IndexOutOfRangeException
) - 0 で割り算しようとしたとき(
DivideByZeroException
) - ユーザーの入力が数値でないとき(
FormatException
)
例外処理をするべき箇所
例外処理をすべき場所は、外部環境に依存している処理や、ユーザー入力に応じた処理など、失敗する可能性がある部分である。
たとえば、以下のような処理では例外を考慮する必要がある:
- ファイルの読み書き
- ネットワーク通信(API 呼び出しなど)
- データベースとのやりとり
- ユーザー入力の解析や数値変換
- 配列・リスト・辞書などのコレクション操作
これらの処理は、常に成功するとは限らないため、例外処理によって適切に対処しておくことが重要である。
例外処理の書き方
C#における基本的な例外処理は、try-catch-finally
構文を使用する。
try
{
// 例外が発生する可能性のある処理
int result = 10 / 0;
}
catch (DivideByZeroException ex)
{
// 例外が発生した場合の処理
Console.WriteLine("0で割り算しようとしました。");
}
finally
{
// 成否にかかわらず最後に必ず実行される処理
Console.WriteLine("処理が終了しました。");
}
-
try
:例外が発生するかもしれない処理を記述する -
catch
:例外が発生したときの対応を記述する -
finally
:例外の有無にかかわらず、最後に実行される処理を記述する(省略可能)
例外の伝播
例外が発生したとき、それが catch
によって処理されない場合、その例外は呼び出し元のメソッドへと伝わっていく。この流れを「例外の伝播(でんぱ)」と呼ぶ。
以下のコードは、C
メソッドで発生した例外が B
、さらに A
へと伝播していく様子を示している。
void A()
{
try
{
B();
}
catch (Exception ex)
{
Console.WriteLine($"Aで例外をキャッチ: {ex.Message}");
}
}
void B()
{
C();
}
void C()
{
throw new Exception("Cでエラーが発生しました");
}
このように、例外は処理されるまでスタック(呼び出し階層)をさかのぼって伝播していく。いずれかの場所でキャッチされなければ、最終的にプログラムは強制終了する。
再スロー(rethrow)の活用
catch
ブロック内で例外を補足したあと、さらにその例外を上位に投げ直すことを「再スロー」と呼ぶ。
正しい再スローの例
try
{
DoSomething();
}
catch (Exception ex)
{
Console.WriteLine("ログ出力: " + ex.Message);
throw; // 元の例外情報を保持して再スロー
}
このように throw;
を使用すれば、例外が発生した元の位置(スタックトレース)を維持したまま、上位に伝えることができる。
throw ex;
によるスタックトレースの破壊
よくある誤り:catch (Exception ex)
{
Console.WriteLine("ログ出力: " + ex.Message);
throw ex; // NG:スタックトレースがこの行から始まってしまう
}
このように throw ex;
としてしまうと、例外の発生場所が現在の catch
ブロックであるかのように扱われてしまうため、本来の発生箇所が失われてしまう。これは、デバッグやログ解析に悪影響を与える。
スロー方法 | スタックトレース保持 | 説明 |
---|---|---|
throw; |
保持される | 元の例外発生箇所がそのまま記録される |
throw ex; |
上書きされる | 発生元が catch ブロックに書き換わる |
再スローの使いどころと注意点
再スローは次のような場面で有効である:
- 下位層でログ出力だけ行い、処理の判断は上位層に委ねたいとき
- 特定の例外は処理し、それ以外はそのまま伝播させたいとき
- ライブラリ側で例外をラップせず、そのままアプリケーション側に投げたいとき
ただし、不要な再スローや、すべての例外を再スローするような乱用は避けるべきである。**再スローは「責任の委譲」**であることを意識し、処理の責任範囲を明確にした設計を行うべきである。
例外処理のアンチパターン
例外処理は便利だが、使い方を誤るとバグの温床になる。ここでは、よくあるアンチパターンを紹介する。
エラーを握りつぶす(ログもスローもなし)
try
{
ProcessData();
}
catch (Exception)
{
// 何もせず無視するしたり、真偽値のみ返したいする
}
これは「エラーの握りつぶし」と呼ばれる危険なパターンである。エラーが発生しているにもかかわらず、それに対する対応が行われないため、バグに気づくきっかけすら失われる。
最低限、ログ出力や通知などで状況を記録すべきである。
try
{
ProcessData();
}
catch (Exception ex)
{
Console.WriteLine($"エラーが発生しました: {ex.Message}");
}
また、必要であれば例外を再スローして、上位に通知することも考慮する。
全体でエラーをハンドリングするとなおよし。
catch (Exception ex)
{
Console.WriteLine($"致命的エラー: {ex.Message}");
throw;
}
何でもかんでも Exception でまとめてキャッチする
catch (Exception ex)
{
Console.WriteLine("何かのエラーが発生しました。");
}
このように、すべての例外を Exception
一つでまとめてキャッチすると、具体的な原因が分かりにくくなり、適切な対応ができなくなる。
想定される例外は個別にキャッチすべきである。
catch (FileNotFoundException ex)
{
Console.WriteLine("ファイルが見つかりません。");
}
catch (FormatException ex)
{
Console.WriteLine("入力の形式が不正です。");
}
例外に頼りすぎる
本来であれば事前に条件をチェックすべきところで、例外処理に頼ると、意図が不明瞭になり、パフォーマンスにも悪影響を与える。
try
{
var item = list[10]; // インデックス範囲外
}
catch (ArgumentOutOfRangeException)
{
// 何もしない
}
このような場合は、以下のように条件分岐で対処するのが適切である。
if (list.Count > 10)
{
var item = list[10];
}
おわりに
初心者のうちは「とりあえず try-catch を書く」ような使い方になりがちだが、
例外の仕組みと役割をしっかり理解した上で、「どこで・なにを・どうやって処理するのか」 を明確に意識することが重要である。
Discussion