例外処理 - るとさんの投稿への返事

id:lethevert:20050715:p1 の記事に対して、るとさんからいただいたコメントに対し、ちょっと考察しようかなと思います。(コメント中に、(*1)という形で、マークをつけています)

# ると 『はじめまして。トラックバックを送れる環境にないのでコメントで失礼します。Cleanを調べていて来ました。日本語対応化、素晴らしいです。

例外の話ですが、結論を先に書くとJavaでは

void main()
{
  int x = -1;
  if(x <= 0){
  System.err.println(”入力値の範囲が不正です”);
  }else{
    try{
      x = exceptionalFunction(x);
      System.out.println(”=======結果=======”);
      System.out.println(x);
    }catch(SomeException e){
      throw new VirtualMachineError();
    }
  }
}

とするのが良いコードです(*1)。またはexceptionalFunctionで例外を投げない(*2)というのも考慮するべきです。

このサンプルコードが変に思えるのは「その例外は本当に例外的なのか、そうならばどう例外的なのか」ということをコードのレベルごとに考慮せずに例外を使っているためです。(*3)

例外というのは「例外的な処理」に使うものなのでただの分岐とは違います。

例外は例外的な場合にのみ投げるべきで、想定した範囲内であれば例外を投げずに内部で処理します。そしてそのレベルではどうしようもないときにのみ上位に例外を投げます。

なのでexceptionalFunctionは、x<=0という状況が自分のレベルで考えて例外的であり、処理が続行できなかったり明らかに無意味な値となるかどうかを考えてから例外を投げるべきです。ここで「自分のレベルで考えて」というのはmainから見れば無意味な値でも自分から見れば、例えば他の関数から呼び出された場合には意味のある値になるかどうかなどを考慮するということです。
mainでも、x<=0という状況が想定の範囲内であれば事前にチェックをしエラーメッセージを出すなどするべきで、例外は投げるべきではありません。
一方、x<=0がmainのレベルでも例外的な状況であり、例外を上位に投げるにしても注意が必要です。

同じ例外を投げるにしてもレベルによってその意味が変わる場合があります。0除算例外を例にとると割り算のルーチンから見ればそれはただの0除算ですが、上位のコードでそれがファイルの内容が壊れていることが原因だと分っていればそれはファイルフォーマット例外となります。なので0除算例外をそのまま上位に伝搬させずに別の例外を投げ直すべきです。

サンプルを見ると(exceptionalFunctionはあくまで例でx<=0だと計算が続行できないと仮定すると)エラーメッセージからxはユーザーからの入力値と分るのでx<=0というのは想定の範囲内であり、事前にチェックをしなければなりません。そして、else節の中で例外が起きた場合はVMのエラーとなりますので「入力値が不正」というメッセージを出してはならず、 VirtualMachineErrorを投げます。

それでも、VirtualMachineErrorを投げるコードは冗長なので、投げなければならない状況を判別して、投げるコードを自動挿入して欲しいというのはもっともな意見です。
あと、実際にはJavaのクラスライブラリはここまで考えて作ってない場合も多いと思います。便利ならそれでいいや、という感じで。

以上の話はピアソン・エデュケーションから出ている『達人プログラマー』を元にしたものですが、『達人プログラマー』はコードのレベルの観点からは書かれていませんし、例外の種類を変えて投げ直す話も出てきません。あと『達人プログラマー』ではファイルを開く時に見付からないことが例外になる場合とならない場合の例が出てきますが、ファイルの存在を確認してから開くまでの間にファイルが削除される場合とか考えるとごちゃごちゃするのであまり良い例ではないです。でもおもしろい本なので読んでみるといいと思います。』 (2005/07/18 03:06)

*1

確かに、このケースでは、これがよさそうですね。でも、常に、このやり方を形式的に踏襲できるんでしょうか? VirtualMachineErrorを使うという点に、ちょっと強引さを感じます。あと、「if(x <= 0){」の記述を形式的に保証できていない点は、結局解決されていないです。

*2

関数の呼出元が特定できないので、例外を投げないということは、明らかによいプログラミングではないです。Javaの機構の中で、呼出元の関数に対して、形式的な制約をかけることができる機構が、例外しかない以上、throws句を書くことは必須だと思います。

*3

ここから下の議論は、僕のオリジナルの議論から大きく外れていると思います。
僕のオリジナルの議論は、Javaの例外処理の機構だけでは、不完全であるどころか、書き方によっては、例外機構を利用する方が気持ちの悪いコードを生成してしまいかねないということを述べていました。そして、id:emeitchさんの指摘によって、僕とほぼ同じ問題意識を持っている人が何年も前からいて、それが「Design by Contract」という考え方に集約され、Eiffelというプログラミング言語として実現されていて、さらに、最近、Javaでもその欠陥が意識され始めて、「Design by Contract」をJavaに取り入れる議論が続いているということが分かったのでした。
つまり、関数の定義域値域(そうそう、この言葉ですよ、僕の考えていた概念は!)を宣言するという方法を取る方が自然なケースを、Javaで記述しようとしたときに、その表現方法として例外機構を使うことで起こる不自然さについて考察していたのです。
関数ごとにそれぞれのコンテクストがあって、そのコンテクストによって例外の解釈が変化し、それを例外の種類を変えることで表現するということは、例外を使う上で、基礎的で重要な例外の使用方法ですが、それは、今回の議論のテーマではありませんでした。
ちなみに、これはまた別の話ですが、コンテクストによって、例外の種類を変えて投げなおすということを、実際にきれいに書くことは、結構難しいと思います。複数人で共同開発している場合の方針のすり合わせもありますし、例外クラスを新しく定義することにコストがかかることと、逆に例外クラスの増加を制御しづらい(Javaの仕様では、例外クラスのスコープを切り分け難いと思います)ということもあります。