例外処理について、私はこう思う

他人の意見の翻訳ばかりでもアレなので。

例外処理機構についての私の考え方は、Diksamがそうなっているように、

  1. 例外処理機構自体は必要
  2. 検査例外も必要

というものです。

例外処理自体の有用性について

Joel Spolsky氏の記事の翻訳の感想でも書いたのですが、例外処理機構は必要だと思います。「戻り値でちまちまエラーケースを上位に戻していってうまくいくと思えるほど、私は(自分を含む)プログラマを信用していない」ためです。

Joel氏は

例外を上げるかもしれない関数を呼び、それをその場でcatchしないときはいつも、あなたは、データを整合性のない状態にしたまま突然中断された関数や、あなたが考慮しなかった別のコードの実行経路による驚くようなバグが発生する機会を作り出しているのだ。

と書いていますが、こういうケースは実際に存在します。たとえば(これは「プログラミング言語を作る」に書いた例ですが)何らかのツリー構造を作っているとして、

  node.childlen = new Node[5];
  for (i = 0; i < 5; i++) {
      node.children[i] = new Node();
  }

子のノードを5個追加しようとしていますが、これが3個目で失敗した場合、3回目の「new Node()」で例外が発生することになります。この例では、親ノードに対し「node.childlen = new Node[5];」を先に実行してしまっていますから、node.children[3]以降がnullになることになります。データ構造上、これを許さないケースは多いでしょう。子がいないなら、node.childrenはnullにするとか、長さ0の配列を割り当てておくのが普通です。たとえば以下のように書く必要があるはずです。

  Node[] temp = new Node[5];
  for (i = 0; i < 5; i++) {
      temp[i] = new Node();
  }
  // node.chlidrenを最後まで触らないので、
  // node.childrenが子がいないことを示す状態で初期化されていれば、
  // 例外が発生しても、データの不整合を起こすことはない。
  node.childlen = temp;

でも、例外処理機構を使っていると、こういうところの検証がえらく難しい、というのがJoel氏の指摘なのだと思います。

しかし、じゃあリターンコードでステータスをちまちま上位に返していけばこういうところが万全になるかというと、私にはちょっとそうは思えない。化数やメソッドの呼び出し階層を、「上司が部下に仕事を依頼する」ことにたとえるとするならば、リターンステータスで異常を返すことは、部下が上司に異常を報告していることを意味します。それは結構なのですが、上司は、いとも簡単にそれをもみ消すことができる。しかも、リターンステータスをいちいちチェックしなければ「自動的にもみ消す」わけですから、これはデフォルトがもみ消すほうに振ってある状態です。こういうやり方では、下っ端が、たとえば原子炉の放射能漏れに気付いたりとかしても、上司から上司へ報告しているうちにいつの間にかもみ消されてしまうのが目に見えています。とても推奨できない。

例外処理機構があれば、(検査例外の有無に関わらず)例外を「もみ消す」時には、どこかの階層で、明示的に「もみ消す」コードを書かなければなりません。例外をもみ消すのなら、陽に、その階層の責任においてやれよ、ということかと思います。もっとも、Javaでも、Exceptionを捕まえる空のcatch節を書いて、(Error以外の)あらゆる例外をもみ消すコードは山ほど見ましたから、いっそ例外クラスはnewされた時点で処理系レベルで強制的にログぐらいは吐いたほうがよいのかもしれませんね。

検査例外の話

検査例外と言うか、Javaで言うところのExceptionとRuntimeExceptionの使い分けの話になりますが。

これについては、私はかつて「Java謎+落とし穴徹底解明」で以下のようなことを書きました。


  1. Errorは、「回復が難しいか不可能」なところに使えとJLSに書いてある。
  2. RuntimeExceptionは、「どこでも発生し得るからいちいちthrowsに書いてはいられないが、アプリケーションがcatchする可能性がある例外」、ぶっちゃけバグだ*1
  3. それ以外のExceptionは、「ユーザの誤操作など、プログラムにバグがなくても発生する可能性があり、かつそれに対して必ずプログラムがきちんと対応しなければならない場合に使うべき」

この考えは今も変わっていません。

おそらく同じようなことを、赤間さん*2は以下のように書いておられます。

.NETとJavaの例外処理の違い – とあるコンサルタントのつぶやき

Java には検査例外と実行時例外と呼ばれる 2 種類の例外が存在しており、言語仕様として業務エラーを例外(検査例外)として取り扱える仕組みを持っている

  • メソッドシグネチャとして throws 句を書かないとダメ。
  • 呼び出し側で try-catch を書かないとダメ。

この 2 つの特徴は、要するにこの検査例外が、「必ず処理ルートとして考慮しなくちゃいけないケースである」ということを意味しており、この特徴はそのまま業務エラーに当てはまります。つまり、業務エラーとはそもそもどのようなものだったのかというと、

メソッド側(上の例でいうと BC 側)では、インタフェース仕様(メソッド仕様)の一部として定義しなければならないもの。
呼び出し側(上の例でいうと UI 側)では、必ず後処理してメッセージなどを表示しなければならないもの。
でした。CLR 系言語(C#VB)では、言語仕様としてこのような業務エラーを体系的に取り扱える仕組みがないため、やむなく enum 値や構造体クラスなどを使って業務エラーを表現していたのですが、Java の場合には、検査例外を使えば言語仕様として業務エラーを体系的に取り扱える、ということになります。

これについてはまったく同意で、まさにわが意を得たり! と思ったのですが――
JavaのInteger.parseInt()について以下のような記述があって、

さらにつぶやいておくと、このことからわかるように、基本的な考え方として、汎用クラスライブラリの戻り値を設計する際には、

「業務エラーかどうかが状況次第で変わるものについては、かたっぱしからアプリケーションエラーに倒して設計しておくべき。」

なのだと思います。この点に関しては Java のクラスライブラリには問題があって、特に I/O 系の業務エラー(RemoteException や SQLException)が片っ端から検査例外として実装されてしまっているのはかなり困りものなのですよね....。(実装コードが非常に書きにくくなるため。これは Java をいじっているときの不満事項の一つでした。) 検査例外が .NET にないのは悔しいものの、不適切な検査例外の利用は逆にデメリットにもなるので、この辺のトレードオフが悩ましいところです。

これには首をひねったのでした(私は、5冊しかない著書の実に2冊において、「NumberFormatExceptionが検査例外じゃないのはどう考えても設計ミスだろ、ということを書いていますので)*3

「安全側に倒す」なら、例外をもみ消してはいけない。前述の通り、例外をもみ消すなら、陽に、その階層の責任においてやれよ、と私は思いますので。

それよりも

いやさ、JavaとかC#とか、ついでにDiksamとかの例外処理機構の使いにくさは、検査例外云々よりもむしろ、catch節の書きにくさにあると思うのですが…… 例外A, B, Cが飛んでくる可能性があって、どのケースでもとりあえずログは吐きたい、かつCのケースだけ特別な処理を書きたい、という場合、現状のtry catchの構文だと、ログを吐くコードを3つのcatch節にコピペする必要があります*4。面倒だからExceptionをcatchして、catch節の中でinstanceofで処理を分けたくなります。このへんをうまく書ける構文があれば、Diksamにも採用したいと思うのですけれど。

書ききれなかった

検査例外を不要とする主張に対する反論として書きたいことがあったのですが*5、眠いのでギブアップです。数日中に書きます。

あと、まあ、あれだ。

宣伝ですが、ぜひこちらも。

*1:というわけでDiksamには、RuntimeExceptionはありませんが、BugExceptionというクラスを処理系が提供しています。これは本来catchを禁止すべきだと思うのですが、アプレットのバグでWebブラウザが死んだり、サーブレットのバグでサーブレットコンテナが死んだりしては困るのでcatch可能としています。

*2:.NETに関係すれば絶対にこの方の本を読むことになります。

*3:それはそれとして、「アプリケーションエラー」といえば一般には「業務エラー」を指すのではないでしょうか。システムエラーとアプリケーションエラーの違いがよくわかりません。.NET Frameworkにおいてはいつの間にかApplicationExceptionの継承が非推奨になっていますが、Diksamでは、ApplicationExceptionクラスは存在し、かつそれは検査例外です。

*4:なお、例外を投げる側の想定と、それを使う側の想定はたいてい食い違うので、例外のクラス階層はcatchする側ではあんまり役に立ちません。

*5:や、たいしたことじゃないです。今まで書いているのと同じことです。