オブジェクト指向超入門〜第7回〜
例外というのはエラーが発生した時の通知の方法であって、オブジェクト指向とは直接関係のない文法のように思われますが、実はオブジェクト指向にとって無くてはならない大事な機能です。今回は例外について考えてみます。
まず例外の文法について今一度確認しましょう。C++言語で例外が登場した頃、入門書などでは「エラー処理に埋もれた見にくいコードをスッキリ見やすくするため」の文法なのだという説明がよく書かれていました。
ret = funcA();
if(ret != 0) {
// エラー処理
return;
}
ret = funcB();
if(ret != 0) {
// エラー処理
return;
}
ret = funcC();
if(ret != 0) {
// エラー処理
return;
}
このように関数内でのエラーの発生を戻り値で通知しようとすると、コードがエラー処理だらけになってしまい、肝心の処理内容が見にくくなってしまいます。そこで以下のようにエラー処理を一箇所にまとめてスッキリさせようという事です。
try {
funcA();
funcB();
funcC();
} catch(Exception) {
// エラー処理
return;
}
これはこれで確かにメリットの1つではありますが、この為に例外という文法を作ったのだと考えるのは少し違います。上記のメリットは例外を使う事により結果的にもたらされた副次的な効果、言ってみれば副作用的なものであって、例外には実はもっと本質的な役割があると考えるべきです。
その役割とは何か。エラーの発生を無視できないよう強制させるのが、その最大の目的です。例えばエラーの発生を戻り値で返す前者のコードで
funcA();
と、ただ関数を呼び出すだけで戻り値をチェックしないようなコードを書いたとしましょう。これは何事もなくコンパイルを通りますし、エラーが起こらない以上は正常に実行できます。しかしもし関数の中でエラーが発生した場合、せっかくエラーの発生を呼び元に知らせているのに、呼び出した側はそれをあっさり無視して実行を継続しようとします。関数内で起きたエラーに対する適切な対処を何もしないままプログラムの実行を継続した場合、そのエラーを放ったらかしてたのが原因で後続の処理で意図しない重大な不具合が起きるかもしれません。そしてそのような不具合は往々にしてデバッグが困難な、やっかいな類のバグになります。
プログラマの方ならお分かりでしょうが、不具合の事象が発生した箇所とそれの根本原因となった箇所が遠く離れているほど原因究明が難しくなりがちですよね。
「エラーコードを返すような関数を呼び出す場合は、呼び出し側で必ずエラーの有無をチェックする事」というようなコーディング規約を作って、それをプロジェクトの全メンバーに周知すれば解決する問題でしょうか。チームでシステム開発をしているプロの方ならばお分かりでしょうが、そんな規約は気休め程度にすらなりません。プロジェクト内には破壊的なコードを平気で書いてくれるヘボプログラマがゴロゴロいます。個々のプログラマの良心なんてものに期待してはいけないというのは、オブジェクト指向のポリシーのひとつなのでした。(「構造体の問題点」を参照)
じゃあどうすればいいのか。例外を使う後者のコードでtry〜catchを書かなかった場合、コンパイルを通してくれません。(嘘です。言語によってはコンパイルできます。でもエラー発生時にはちゃんと?落ちてくれます。この辺の詳しい話は後日書きます。)例外を返す関数を呼び出す場合は、必ずその例外をキャッチしてエラー処理を書く事を強制されるのです。
もちろん以下のような逃げ道はあります。
try {
funcA();
} catch(Exception) {
// 何も対処せず、エラーを無視
}
しかしこれは意図的にエラーを無視していると言えます。処理内容によっては、無視してしまって構わない事が分かっているエラーというのは少なからずある訳で、このようなコードになっていれば、少なくともエラーの可能性を知っていながら(それ相応の理由があって)あえて無視しているのだろうな、という想像ができます。(とは言ってもこのようなコードを、単なるサボリで書いてしまうプログラマが実はいたりします。これについても別途書きます。)
そもそも前者のコードで戻り値をチェックしない理由は、最初から戻り値の事なんて気にしていない、という場合の方が多いのではないでしょうか。つまり「意図せず」に無視してしまっているのです。それに比べれば意図せずうっかり無視してしまうのを防げる後者の方が、思わぬバグを入れ込む可能性を少なくする意味で優れていると思います。