例外処理例外処理(れいがいしょり、英語: exception handling)とは、IT業界で用いられる専門用語で、ある抽象レベルにおけるシステムの設計で想定されておらず、ユーザー操作によって解決できない問題に対処するための処理である。例外処理の結果として問題が解決されないとシステム障害になる。システム停止やデータ破損の原因になり、ユーザーに損害を与える可能性があるため、システム開発で例外処理は重要視されている[1][2][3]。 システムの設計で想定されておらず、継続不能や継続すると問題になる様な状態としては、次のようなものが挙げられる。
注意点として、あらゆる例外が抽象レベルに依存せずすべて異常系であるとは限らない。例えばページフォルトはカーネル内部のメモリなど例外が許されない環境下ではエラーとなるが、仮想記憶を採用したOSにおけるユーザプロセスのメモリは常時物理的に存在するとは限らないためページフォルトを正常系として処理する必要がある。また、例外処理中にさらなる例外が発生した場合は、通常なら正常系となる事象が異常系に変わる場合がある。詳しくは#例外のネストを参照されたい。 例外処理の動作と重要性例外処理の動作としては、システムを構成するプログラムの各呼び出し階層で、呼び出し先が想定していない入力値を受け取って問題が起きた場合に、問題に合わせた例外を発生させて呼び出し元に処理を返す。呼び出し元に例外を返す事によって、呼び出し元で問題解決が行われることに期待するが、どの呼び出し階層の例外処理でも問題解決できない場合は、システムの内部状態に矛盾が残り、システム障害となる。例外発生の後、システムが動作していても、例外への対処結果が設計から逸脱している場合、システムの内部状態に矛盾を来しており、ストレージやデータベースやネットワークに無意味なデータを出力する可能性が生じ、データ破損のみならず、連携する他システムにも矛盾が伝播して広範なシステム障害に繋がる可能性がある。従って、例外処理はシステム障害を未然に防ぐ意味で非常に重要である。 例外とエラーの違い例外(exception)はシステム担当者が問題解決を行う必要がある。例外の問題解決手段には例外を無視することも含まれるが、明確な根拠をもって無視する必要がある(設計の一環として一部の例外を無視しても問題ないと判断する,連携する他システムのメンテナンス中で例外発生が不可避な場合はメンテナンス完了を待つ)。例外に対して、ユーザーが解決すべき問題はエラー(error)と呼ぶ(但し、業務システム開発ではエラーを業務エラー、例外をシステムエラーと表現する場合もあり、技術者間での厳密な統一見解は存在しない)[1][2][3]。何らかのシステム開発を行う会社では、例外への対処が不適切であるとユーザーに損害を与えるため、システム障害を発生させるような例外発生は製品の瑕疵として扱う必要がある。 例外安全性あるコード内を実行中の失敗が、メモリリーク、格納データの不整合、不正な出力などの有害な効果を生じないとき、そのコード片は例外安全であると言う。例外安全なコードは例外が発生したとしてもそのコードが備える不変条件を満たさなければならない。例外安全性にはいくつかのレベルがある[4][5]:
言語サポート幾つかのプログラミング言語では組み込みの例外処理機能を用意している。例えば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 (...)
{
}
例外構文には例外が存在しないことを明示する C++の特徴的な構文として、(6)の省略子を用いたcatchが存在する。あらゆる例外を捕捉可能であり、他の tryブロックは(8)のように関数全体に適用することもできる。これをfunction-try-blockと言う。catch文では局所変数を参照できず引数だけしか参照できないが、コンストラクタの初期化リストで発生した例外やデストラクタ[8]から投げられた例外はこの書き方でしか捕捉することができない。 例外処理構文を最初に実装したのはAdaであるが、C++の例外処理構文はJavaやJavaScript、C#など多くの後発言語の規範となった。 例外指定C++の例外指定 (Exception Specification、例外仕様とも) は、関数から伝達される例外の種類を明示する言語機能である[9]。例えば Javaによる例外処理構文の例 public void throwError() throws Exception {
throw new Exception();
}
public void catchException() {
try {
throwError();
} catch (Exception e) {
e.printStackTrace();
}
}
Javaでは例外はクラスとして実装する。例外を「投げる(throw)」メソッドは Smalltalkによる例外処理構文の例| value |
value := "式であるため戻り値が存在する"
[
Notification signal: '接続準備完了'.
1. "本来はvalueに代入されるが例外が発生しているため代入しない"
]
on: Error, Notification "複数の例外を同時に捕捉できる"
do:
[ :exception |
exception return: 0. "1の代わりにvalueに0を代入する。"
]
on: Exception
do:
[ :exception |
exception pass. "処理できない例外は上位の例外処理に委ねる。"
].
言語機能としては例外処理構文が存在しないが別途例外処理を備える言語も存在する。Smalltalkは言語機能として制御構文が殆どない。このため例外処理構文もブロックを組み合わせたメッセージ式として記述するようになっている。言語機能ではないため極めて柔軟性がありブロックの戻り値を変更したり例外が発生した式の途中から復帰したりなど様々な制御が可能になっている。 制御フローへの転用例外処理の過程では処理の流れが通常の制御とは大きく変化することとなるが、これを(エラー処理以外の目的で、正常系において)積極的に利用することは、アンチパターンとされることもある。 一方、Ruby[11]やPython[12]では、イテレータが終端に達するという、無限ループでなければ必ず発生する事象により例外が起きることもあるほか、Rubyでは例外処理と関係なく大域脱出を行う制御構造も用意されている[13]。 Smalltalkでも例外はエラー処理以外の通知として使われる。Smalltalkの例外 (Exception) はエラー (Error) と通知 (Notification) からなり、EOFの通知やスレッド間の割り込み終了通知等に使われている。またSmalltalkは値を検索した結果値が見つからなかった場合の戻り値として 通知例外にはエラー例外と異なる特徴的な点がある。通知を投げた場合は、エラーと異なり補足しなければ処理を中止せずそのまま続行となる。 戻り値と例外例外処理(例外オブジェクト)をサポートしないCなどの言語では、従来から関数(サブルーチン)の戻り値によってその関数(処理)の成否を判定する方法がとられてきた。慣例的に、関数の戻り値を32ビット整数値などで宣言して、関数が成功した場合は0を返し、失敗した場合はエラーコードとして何らかの負数を返すことが多い[14][15]。さらに簡略化して、成否の結果を真偽値1/0で返すだけにすることもある。戻り値がポインタ型である場合は、成功した場合に有効なポインタすなわち非NULLを返し、失敗した場合に無効なポインタすなわちNULLを返すのが通例である。標準ライブラリや各種APIでは、詳細を伝えるエラーコードを別途 このような戻り値による処理の成否判定には下記のような問題点がある。
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) {
if (!isnan(x[i]) && x[i] > 0) { (*outNumberOfElements)++; }
}
return true; // 正常終了。
}
4.の問題では関数の呼び出し結果をいったんローカル変数に格納することなく次の関数引数にそのまま式として渡すようなこともできなくなる。例えば下記のC言語の例では、atof()関数の戻り値が正常系と異常系とで区別がつかない仕様のため、対象フォーマット外の不正な入力があっても検知できず処理を継続してしまう。例外を使わずにこの問題に対処するには、正常系と異常系とを区別できるようにするために、関数の実装およびインターフェイスが複雑化することを許容しなければならない。 #include <stdio.h>
#include <stdlib.h>
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
{
}
}
}
別の例として、たとえば主記憶領域の容量やファイル容量を取得する関数を設計する際、結果を符号なし整数型 (非負数) の戻り値で返すように決めた場合、戻り値でエラー判定用の値を返すことができない。この場合、 言語レベルでの例外処理はこれらの欠点を解消し、例外を確実に、かつ統一的に処理する目的で導入されたものと言える。 回復処理記憶領域の枯渇処理中に使用可能な記憶領域が枯渇した場合の回復は比較的単純である。
記憶領域が原因として上記が考えられるがGUIプログラムのイベント処理であれば3以外の状況から回復できる。1,2どちらも現在実行中の処理を中断して処理に使った記憶領域を解放すればプロセスを継続できる可能性がある。特に2の状況であればシステムの記憶領域使用状況が回復を待って再度同じ処理を実行することができる。 CUIの場合でかつ複数のファイルを処理しているような場合1の状況から回復することができる。枯渇が発生した大容量ファイルの処理は無理でもその後の小容量ファイルは処理できる可能性がある。 次にSmalltalkによる回復処理の例を示す: | window textBox filePath open notifyArea |
"ファイルの読み込み失敗を通知する部品"
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.
例外のネストある例外を処理中に、元のものとは別の例外が発生する場合がある。これを例外のネスト(入れ子)と呼ぶ。例外のネストの処理は例外処理の設計や実装を複雑にするため、どの範囲までネストを許すかやどのような処理に対しては例外を許さないかがしばしば問題となる。 言語仕様として実装されている例外にあっては、ネストのレベルは一般には制限されていない。この場合、同じ例外が繰り返し発生することにより無限ループに陥る恐れがある。そのような状況への対策は設計者や実装者の責任となる。 別の抽象レベルとして、OSやCPUの例外は処理や例外に備えた準備が複雑化することを防ぐため、ネストレベルに制限をかける場合が多い。典型的な例がページフォルトで、ユーザプロセスでのページフォルトは許すが、それを処理するカーネルに対してはページフォルトを許さないことにより例外処理を簡単化している。また、CPUでは例外が発生しハンドラを実行しようとした際、スタックオーバーフローに起因するページフォルトなど更なる例外が発生することがある。これはダブルフォルト(英語: Double fault)と呼ばれ、専用のハンドラやスタックなど一般的な例外よりもさらに複雑な設定や処理を要する。ダブルフォルトを処理しようとしてさらに例外が発生してしまうとトリプルフォルト(英語: Triple fault)となり、現実的な処理が困難となるためリセット処理を行う実装が多い。 参照
関連項目 |