例外処理

例外処理（れいがいしょり、）とは、IT業界で用いられる専門用語で、システムの設計で想定されておらず、ユーザー操作によって解決できない問題に対処するための処理である。例外処理の結果として問題が解決されないとシステム障害になる。システム停止やデータ破損の原因になり、ユーザーに損害を与える可能性があるため、システム開発で例外処理は重要視されている。
システムの設計で想定されておらず、継続不能や継続すると問題になる様な状態としては、次のようなものが挙げられる。
例外処理の動作と重要性.
例外処理の動作としては、システムを構成するプログラムの各呼び出し階層で、呼び出し先が想定していない入力値を受け取って問題が起きた場合に、問題に合わせた例外を発生させて呼び出し元に処理を返す。呼び出し元に例外を返す事によって、呼び出し元で問題解決が行われることに期待するが、どの呼び出し階層の例外処理でも問題解決できない場合は、システムの内部状態に矛盾が残り、システム障害となる。例外発生の後、システムが動作していても、例外への対処結果が設計から逸脱している場合、システムの内部状態に矛盾を来しており、ストレージやデータベースやネットワークに無意味なデータを出力する可能性が生じ、データ破損のみならず、連携する他システムにも矛盾が伝播して広範なシステム障害に繋がる可能性がある。従って、例外処理はシステム障害を未然に防ぐ意味で非常に重要である。
例外とエラーの違い.
例外(exception)はシステム担当者が問題解決を行う必要がある。例外の問題解決手段には例外を無視することも含まれるが、明確な根拠をもって無視する必要がある（設計の一環として一部の例外を無視しても問題ないと判断する，連携する他システムのメンテナンス中で例外発生が不可避な場合はメンテナンス完了を待つ）。例外に対して、ユーザーが解決すべき問題はエラー(error)と呼ぶ（但し、業務システム開発ではエラーを業務エラー、例外をシステムエラーと表現する場合もあり、技術者間での厳密な統一見解は存在しない）。何らかのシステム開発を行う会社では、例外への対処が不適切であるとユーザーに損害を与えるため、システム障害を発生させるような例外発生は製品の瑕疵として扱う必要がある。
例外安全性.
あるコード内を実行中の失敗が、メモリリーク、格納データの不整合、不正な出力などの有害な効果を生じないとき、そのコード片は例外安全であると言う。例外安全なコードは例外が発生したとしてもそのコードが備える不変条件を満たさなければならない。例外安全性にはいくつかのレベルがある:
言語サポート.
幾つかのプログラミング言語では組み込みの例外処理機能を用意している。例えばAda、C++、Java、Scala、C#、JavaScript、OCamlがそうである。これらの言語では専用の言語機能によってプログラマが例外処理を記述する手間を軽減している。
例外が発生したことを見落として正常時の動作を継続してしまうと、より深刻・致命的な異常を招くおそれがある。それを避けるには例外が発生したことのチェックを綿密に行い、例外が検出された場合には適切な事後処理を行う他ない。しかし、大規模なプログラムではこのようなチェックは膨大なものとなり、本来目的としている正常時の処理よりも多くの記述を必要とする場合すらある。
そこで、これらの言語では例外の発生チェックをほぼ自動化している。例外が発生すると現在の処理を中断する。発生した例外の事後処理を担当できるハンドラを探して次々にコールスタック（関数呼び出し）を遡り、適切なハンドラを見つけるとそれに事後処理を任せる。これにより、遡る途中にあったこの例外を処理する能力を持たない処理は自動的に中断されることになる。
Schemeでは言語レベルでの例外処理を持たないが、これは継続が存在するため例外をライブラリレベルで実現できるからである（標準仕様であるSRFI-34で定義されている）。
C++による例外処理構文の例.
void Function0(void) throw(...) // (2)
 // (1)
 throw 0;
 throw "message";
 throw std::runtime_error( "message" );
 throw;
void Function1(void)
 try
 Function0();
 catch( int exception ) // (3)
 // 回復処理
 catch( char const *exception ) // (4)
 // 回復処理
 catch( std::exception const &exception ) // (5)
 // 回復処理
 catch(...) // (6)
 // 回復処理
 throw; // (7)
void Function2(void)
try
 // (8)
catch (...)
codice_1ブロック中で呼び出した関数codice_2が(1)のcodice_3を実行すると、codice_4のcodice_5文へと制御が移る。C++では後発の言語とは異なり、std::exceptionあるいはその派生型以外の型の値でもcodice_3で投げることができ、(3)(4)(5)の様に型に対応したcodice_5文で捕捉することができる。なお、(1)では例示のため複数のcodice_3を書いているが、実際には1個目のcodice_3を実行した時点でcodice_5文に移動する。
例外構文には例外が存在しないことを明示するcodice_11が標準化されている。
C++の特徴的な構文として、(6)の省略子を用いたcatchが存在する。あらゆる例外を捕捉可能であり、他のcodice_5が取りこぼした例外でも捕まえる必要がある場合に用いる。値を指定しないcodice_3を捕まえられるのも省略子を用いたcodice_5だけである。また、Microsoft Visual C++といった一部の処理系では、コンパイラオプションの指定によりC++例外だけでなくOSが投げた構造化例外も省略子を用いたcodice_5で捕捉できる。codice_5文の中では(7)のように引数の無いcodice_3を用いた場合、例外の再送を意味する。省略子を用いたcodice_5の場合は例外情報を判断できないため必須であるが、省略子を使わないcodice_5でも派生型の例外を基底型で受け取ってしまった場合のスライシングを防ぐために必要となる。
tryブロックは(8)のように関数全体に適用することもできる。これをfunction-try-blockと言う。catch文では局所変数を参照できず引数だけしか参照できないが、コンストラクタの初期化リストで発生した例外やデストラクタから投げられた例外はこの書き方でしか捕捉することができない。
例外処理構文を最初に実装したのはAdaであるが、C++の例外処理構文はJavaやJavaScript、C#など多くの後発言語の規範となった。
例外指定.
C++の例外指定 (Exception Specification、例外仕様とも) は、関数から伝達される例外の種類を明示する言語機能である。例えば codice_20 はint型のエラーをthrowしうることを明示している。例外指定はコンパイラによる静的例外検査を想定して用意された機能だが各コンパイラには実装されず、C++11で非推奨になり、C++17をもって廃止された。
Javaによる例外処理構文の例.
 public void throwError() throws Exception {
 throw new Exception();
 public void catchException() {
 try {
 throwError();
 } catch (Exception e) {
 e.printStackTrace();
Javaでは例外はクラスとして実装する。例外を「投げる(throw)」メソッドはcodice_21のように指定する。Javaプラットフォーム上でJava言語を使用し、発生する例外がcodice_22を継承しているが、しかしjava.lang.RuntimeExcptionを継承していない場合、try/catch文で例外処理を明示的に記述するか、メソッドにthrowsを追加する必要がある。ただし、Javaプラットフォーム上で動く言語でも、GroovyやScalaなど、Java言語以外の多くは、RuntimeException以外の例外に対して必ずしも明示的に記述しなくても良くなっている。初期のJava（JDK1.0）では、I/O処理など他の手段では例外の発生を回避することができない種類の例外に対してはRuntimeExceptionを継承させないという設計思想になっていた。
Smalltalkによる例外処理構文の例.
value := "式であるため戻り値が存在する"
 Notification signal: '接続準備完了'. 
 1. "本来はvalueに代入されるが例外が発生しているため代入しない"
 on: Error, Notification "複数の例外を同時に捕捉できる"
 do:
 [ :exception |
 exception return: 0. "1の代わりにvalueに0を代入する。"
 on: Exception
 do:
 [ :exception |
 exception pass. "処理できない例外は上位の例外処理に委ねる。"
言語機能としては例外処理構文が存在しないが別途例外処理を備える言語も存在する。Smalltalkは言語機能として制御構文が殆どない。このため例外処理構文もブロックを組み合わせたメッセージ式として記述するようになっている。言語機能ではないため極めて柔軟性がありブロックの戻り値を変更したり例外が発生した式の途中から復帰したりなど様々な制御が可能になっている。
制御フローへの転用.
例外処理の過程では処理の流れが通常の制御とは大きく変化することとなるが、これを（エラー処理以外の目的で、正常系において）積極的に利用することは、アンチパターンとされることもある。
一方、RubyやPythonでは、イテレータが終端に達するという、無限ループでなければ必ず発生する事象により例外が起きることもあるほか、Rubyでは例外処理と関係なく大域脱出を行う制御構造も用意されている。
Smalltalkでも例外はエラー処理以外の通知として使われる。Smalltalkの例外 (Exception) はエラー (Error) と通知 (Notification) からなり、EOFの通知やスレッド間の割り込み終了通知等に使われている。またSmalltalkは値を検索した結果値が見つからなかった場合の戻り値としてcodice_23 (null) を返さない傾向があり、値が見つからなければ例外を投げる。ただし単純な例外処理というパターンがあり、値が見つからない場合でも例外構文を使わず安全に回復する手段を用意している。
通知例外にはエラー例外と異なる特徴的な点がある。通知を投げた場合は、エラーと異なり補足しなければ処理を中止せずそのまま続行となる。
戻り値と例外.
例外処理（例外オブジェクト）をサポートしないCなどの言語では、従来から関数（サブルーチン）の戻り値によってその関数（処理）の成否を判定する方法がとられてきた。慣例的に、関数の戻り値を32ビット整数値などで宣言して、関数が成功した場合は0を返し、失敗した場合はエラーコードとして何らかの負数を返すことが多い。さらに簡略化して、成否の結果を真偽値1/0で返すだけにすることもある。戻り値がポインタ型である場合は、成功した場合に有効なポインタすなわち非NULLを返し、失敗した場合に無効なポインタすなわちNULLを返すのが通例である。標準ライブラリや各種APIでは、詳細を伝えるエラーコードを別途codice_24のようなグローバル変数に格納することもある。各エラーコードによって失敗の原因を定義しておき、呼び出し側で原因を判定する。
このような戻り値による処理の成否判定には下記のような問題点がある。
3. に関連する問題として、戻り値が正常系の結果取得に使えないため引数を処理結果の取得用に使い関数インターフェイスおよび呼び出し側のコードが複雑化するという問題がある。
bool countPositiveElements(const double x[], int inNumberOfElements, int* outNumberOfElements) {
 if (x == NULL || inNumberOfElements <= 0 || outNumberOfElements == NULL) {
 return false; // 異常終了。
 *outNumberOfElements = 0;
 for (int i = 0; i < inNumberOfElements; ++i) {
 return true; // 正常終了。
4.の問題では関数の呼び出し結果をいったんローカル変数に格納することなく次の関数引数にそのまま式として渡すようなこともできなくなる。例えば下記のC言語の例では、atof()関数の戻り値が正常系と異常系とで区別がつかない仕様のため、対象フォーマット外の不正な入力があっても検知できず処理を継続してしまう。例外を使わずにこの問題に対処するには、正常系と異常系とを区別できるようにするために、関数の実装およびインターフェイスが複雑化することを許容しなければならない。
double addAsDouble(const char* x, const char* y) {
 return atof(x) + atof(y);
int main(void) {
 printf("%f\n", addAsDouble("1", "2"));
 printf("%f\n", addAsDouble("x", "y")); // 変換および加算結果は0となるが、無意味。
一方、文字列から数値への変換に失敗した場合に例外を投げるライブラリを持つ言語では、メインロジックに関係のないコードを挿入することなく、正常系と異常系とを簡潔かつ明確に区別できる。下記はC#による例である。
using System;
public class Test
 public static double AddAsDouble(string x, string y)
 return double.Parse(x) + double.Parse(y);
 public static void Main()
 try
 Console.WriteLine(AddAsDouble("1", "2"));
 Console.WriteLine(AddAsDouble("x", "y")); // 例外がスローされ、処理は継続されない。
 catch
別の例として、たとえば主記憶領域の容量やファイル容量を取得する関数を設計する際、結果を符号なし整数型 (非負数) の戻り値で返すように決めた場合、戻り値でエラー判定用の値を返すことができない。この場合、codice_24のようなグローバル変数もしくは別途用意した関数引数経由でエラーコードを返して、呼び出し側で判定する必要がある。たとえばWindows APIのGetFileSize()関数は戻り値でファイルサイズを返すが、混合した設計となっており、エラーが発生した場合はcodice_26を返す。しかし戻り値の型はcodice_27つまり符号なし32ビット整数型であるため、実際にはcodice_28が返却される。これは本来正常値としてありうる値であり、異常系との区別が付かないため、エラーによる結果だったかどうかを判定するにはGetLastError()関数の呼び出しが別途必要になっている。なおcodice_29関数は成否を戻り値で、正常系出力を引数で返す設計となっており、GetFileSize()関数の代替として推奨されている。また、C言語において身近な例としてはcodice_30およびcodice_31、codice_32関数があげられる。これらは確保を要求する容量として0を指定したときC言語の規格としてNULLを返して良いと定義されている。このためこれらの関数の戻り値では記憶空間の容量不足か引数に0を指定したか判断できず切り分けのためにcodice_24を調べるか引数を調べる必要がある。C++において同等のnew演算子ではこの点を容量不足のときだけ例外を投げることによって改善している。
言語レベルでの例外処理はこれらの欠点を解消し、例外を確実に、かつ統一的に処理する目的で導入されたものと言える。
回復処理.
記憶領域の枯渇.
処理中に使用可能な記憶領域が枯渇した場合の回復は比較的単純である。
記憶領域が原因として上記が考えられるがGUIプログラムのイベント処理であれば3以外の状況から回復できる。1,2どちらも現在実行中の処理を中断して処理に使った記憶領域を解放すればプロセスを継続できる可能性がある。特に2の状況であればシステムの記憶領域使用状況が回復を待って再度同じ処理を実行することができる。
CUIの場合でかつ複数のファイルを処理しているような場合1の状況から回復することができる。枯渇が発生した大容量ファイルの処理は無理でもその後の小容量ファイルは処理できる可能性がある。
次にSmalltalkによる回復処理の例を示す:
"ファイルの読み込み失敗を通知する部品"
notifyArea := LabelStringMorph new.
"ファイルを開くための部品"
textBox := TextMorph new.
filePath := TextMorph new.
open := SimpleButtonMorph new.
"ボタンをクリックしたらファイルを読み込んでテキストボックスに表示できるようにする"
open
 on: #click
 send: #value
 to:
 [
 filePath contents asFile withReadStreamDo:
 [ :readStream |
 textBox contents: readStream contents.
 ]
 記憶領域が枯渇した場合の回復処理。
 処理を中断した上、できるだけ資源を解放したのち使用者に記憶領域の解放を促す。
 例外が発生してから例外を捕捉するまでに幾分かの記憶領域が解放される、
 解放後ゆとりができていれば回復できるが、ゆとりが無い場合は回復できなくなる場合もある。
 on: AllocationFailure
 do:
 ObjectMemory compact. "ガーベッジコレクターによる解放"
 textBox contents: ". "途中まで読み込んでしまった文字列を解放"
 ObjectMemory compact. "textBox contents分の解放"
 notifyArea text: '記憶領域が枯渇しているためファイルを開けませんでした。他のプログラムを終了してから再度実行して下さい。'
 ].
"ファイルを表示するテキストボックスとファイルの読み込みに必要な各種部品を組み込んだWindowを表示する"
window := SystemWindow new.
window
 addMorph: textBox;
 addMorph: filePath;
 addMorph: open;
 addMorph: notifyArea;
 openInWorld.