例外処理
例外処理(れいがいしょり)とは、プログラムがある処理を実行している途中で、なんらかの異常が発生した場合に、現在の処理を中断(中止)して、別の処理を行うこと。その際に発生した異常のことを例外と呼ぶ。
何を持って「異常」とするかは様々であるが、次のようなものが挙げられる。
- ハードウェアの故障
- オペレーティングシステム等、システムの設定ミス
- ライブラリの欠損
- ユーザーの入力間違い
- 数値入力を要求している場合での、英単語の入力
- 存在しないファイルの指定
- 許されない演算(0での除算や実数演算で解が虚数になる演算など)
- 割り当てられていない記憶領域へのアクセス
- 不正な値が与えられたポインタで参照する、或いは機械語レベルで不正な値が与えられたインデックスレジスタ等を用いてメモリ参照することとなった場合
- ページフォールト
- オブジェクト指向言語において、オブジェクトとして定義されていないポインタ(Nullポインタ)をオブジェクトとして取扱おうとした場合(NullPointerException)。
例外安全性
あるコード内を実行中の失敗が、メモリリーク、格納データの不整合、不正な出力などの有害な効果を生じないとき、そのコード片は例外安全であると言う。例外安全なコードは例外が発生したとしてもそのコードが備える不変条件を満たさなければならない。例外安全性にはいくつかのレベルがある:
- 失敗透明性、もしくは不送出保証: 操作は成功するものと保証され、例外的な状況の中であっても全ての要求を満たす。もし例外が発生したとしても、その例外をより上位に送出はしない。(最高レベルの例外安全性)
- コミット・オア・ロールバックセマンティクス、強い例外安全性あるいは無変更保証: 操作は失敗することがあるが、失敗した操作は副作用を起こさないことが保証され、すべてのデータは元の値を保持する。[1]
- 基本例外安全性: 失敗した操作の不完全な実行によって副作用が起こることがあるが、状態の不変条件は保たれる。あらゆる格納データは、もはや実行前とは異なるとしても、有効な値を持つ。
- 最小限の例外安全性あるいはリークしない保証: 失敗した操作の不完全な実行によって不正なデータが格納されるかもしれないが、クラッシュしたりリソースがリークしたりすることはない。
- 例外安全性なし: 何も保証されない。(最低レベルの例外安全性)
言語サポート
幾つかのプログラミング言語では組み込みの例外処理機能を用意している。例えばAda、C++、Java、C#、JavaScript、OCamlがそうである。これらの言語では専用の言語機能によってプログラマが例外処理を記述する手間を軽減している。
例外が発生したことを見落として正常時の動作を継続してしまうと、より深刻・致命的な異常を招くおそれがある。それを避けるには例外が発生したことのチェックを綿密に行い、例外が検出された場合には適切な事後処理を行う他ない。しかし、大規模なプログラムではこのようなチェックは膨大なものとなり、本来目的としている正常時の処理よりも多くの記述を必要とする場合すらある。
そこで、これらの言語では例外の発生チェックをほぼ自動化している。例外が発生すると現在の処理を中断する。発生した例外の事後処理を担当できるハンドラを探して次々にコールスタック(関数呼び出し)を遡り、適切なハンドラを見つけるとそれに事後処理を任せる。これにより、遡る途中にあったこの例外を処理する能力を持たない処理は自動的に中断されることになる。
Scheme では言語レベルでの例外処理を持たないが、これは継続が存在するため例外をライブラリレベルで実現できるからである(標準仕様であるSRFI-34で定義されている)。
以下は C#(.NET Framework)のtry、catch の例である。
try
{
//例外が発生する可能性のある処理
}
catch
{
//例外が発生した原因の処理
}
Javaでは例外はクラスとして実装する。例外を「投げる(throw)」メソッドは throws Exception のように指定する必要がある。C++やC#とは異なり、例外を発生するメソッドを呼び出す際にはすべて、try-catch文で例外処理を明示的に記述する必要がある。
public void throwError() throws Exception
{
throw new Exception ();
}
public void catchException()
{
try
{
throwError();
}
catch(Exception e)
{
e.printStackTrace();
}
}
戻り値と例外
例外処理(例外オブジェクト)をサポートしないCなどの言語では、従来から関数(サブルーチン)の戻り値によってその関数(処理)の成否を判定する方法がとられてきた。慣例的に、関数の戻り値を32bit整数値などで宣言して、関数が成功した場合は0を返し、失敗した場合はエラーコードとして何らかの負数を返すことが多い。そして、各エラーコードによって失敗の原因を定義しておき、呼び出し側で原因を判定する。このような戻り値による処理の成否判定には下記のような問題点がある。
- 戻り値は無視できるため、呼び出し先でエラーが発生しても通常通り処理を継続するプログラムを記述できてしまう。
- エラーコードはたいてい32bitの整数値でしかないため、それ以上の詳細な情報(例えばエラーメッセージ)を付加することができない。
- 戻り値を毎回チェックする判定文を記述するのが煩雑である。
言語レベルでの例外処理はこれらの欠点を解消し、エラーを確実に、かつ統一的に処理する目的で導入されたものと言える。