D でのエラー処理
どんなプログラムでも、エラーを扱う必要があります。エラーとは、 プログラムの通常動作の一環ではない、期待されない状態のことです。 よくあるエラー状態の例としては:I came, I coded, I crashed. -- Julius C'ster
- メモリ不足
- ディスク領域不足
- 不正なファイル名
- 読み取り専用ファイルへ書き込もうとした
- 存在しないファイルを読み込もうとした
- サポートされていないシステムサービスを要求した
エラー処理にまつわる問題
伝統的なCのエラー検知/報告の方法は、伝統でも何でもなくて、 関数ごとに異なる場当たり的なものです。以下のようなパターンがあります:- NULLポインタを返す
- 0を返す
- 非0のエラーコードを返す
- グローバル変数errnoをチェックするよう要求する
- 前の呼び出しが失敗していたかどうかを、 次の関数呼び出しでチェックする
きちんとエラー処理をしたコードは、それでなければ綺麗でまとまった実装を、 雑然と散らかったものに変えてしまう傾向があります。
もっと悪いことに、よくエラー処理をしたコードというのはそれ自体がエラーを生みやすく、 プロジェクト中で一番テストされていない(それゆえバグの多い)部分となるため、 単純に省略されてしまいがちなのです。こうなると結局、何か予想されないエラーを プログラムが扱いきれず、"死の青画面" に突入してしまうはめになります。
ささっと適当なコードで済ましてしまうような小プログラムには、 ややこしいエラー処理を書くコストを払うだけ価値がなく、そのようなユーティリティは、 刃ガードのついていないノコギリのような存在になっていまいます。
エラー処理に必要な哲学と方法論は、次の通りです:
- 標準化されている - 一貫して使用することで、より使いやすくなる
- プログラマがエラーチェックをさぼったとしても、 ある程度意味のある結果を出力する
- 古いコードを新しいコードを組み合わせるときに、新しいエラーを、 古いコードを書き換えずとも扱えるようにする
- 不注意でエラーが無視されることをなくす
- 'ささっと適当な' ユーティリティでも正しくエラー処理がなされるようにする
- エラー処理のコードを見やすく書ける
D でのエラー処理の解決策
まず、エラーについてよく考えて、幾つかの仮定を置きましょう:- エラーはプログラムの通常の実行フローにはない。エラーは 例外的で、期待されないものである。
- エラーは通常状態ではないので、エラー処理のコードは さほどパフォーマンスを重視しなくてよい。
- 逆に通常の実行ロジック部分のパフォーマンスが重要である。
- 全てのエラーは何らかの方法で処理される。 明示的に書かれたエラー処理コードか、 あるいはシステムのデフォルト処理か。
- エラー発生部分のコードの方が、エラーから復帰する部分のコードよりも エラーについてよく知っている。
コードが "メモリ不足" のようなエラーを検知したとしましょう。すると、 "メモリ不足" というメッセージを持った Errorオブジェクトが投げられます。 関数呼び出しスタックは、Errorのハンドラが見つかるまで巻き戻されます。 スタック巻き戻しの途中に finally ブロックが実行されます。Errorのハンドラが見つかると、 そこから実行が再開されます。最後まで見つからなければ、 デフォルトのエラーハンドラがよびだされ、メッセージを表示してプログラムを終了します。
これは、先ほどあげた基準にあっているでしょうか?
- 標準化されている - 一貫して使用することで、より使いやすくなる
- ここに上げた方法がD流で、 Dのランタイムライブラリや例で一貫して使われています。
- プログラマがエラーチェックをさぼったとしても、 ある程度意味のある結果を出力する
- エラーに対するcatchハンドラが存在しなければ、 プログラムはデフォルトのエラーハンドラによってキチンと終了し、 適切なエラーメッセージを出力します。
- 古いコードを新しいコードを組み合わせるときに、新しいエラーを、 古いコードを書き換えずとも扱えるようにする
- 古いコードは全てのエラーをcatchするよう書くことも、一部をcatchして 残りは上位関数へ伝えることもできます。どの場合でも、 エラー番号と文字列を相関させる必要はなく、 常に正しいメッセージが与えられます。
- 不注意でエラーが無視されることをなくす
- Error例外は、何とかして処理されます。 エラーを示すためにNULLポインタを返して、その次の行で NULL ポインタを使ってしまう、というようなことはなくなります。
- 'ささっと適当な' ユーティリティでも正しくエラー処理がなされるようにする
- Quick and dirty コードでは、 一切エラー処理を書いたりエラーチェックをしたり する必要はありません。全てデフォルトで、エラーが起きれば 適切なメッセージを表示してからプログラムはきちんと終了します。
- エラー処理のコードを見やすく書ける
- try/catch/finally 文は、延々と続く if (error) goto errorhandler; 文より ずっと見やすいです。
- エラーはプログラムの通常の実行フローにはない。エラーは 例外的で、期待されないものである。
- D の例外処理はぴったりこれに当てはまります。
- エラーは通常状態ではないので、エラー処理のコードは さほどパフォーマンスを重視しなくてよい。
- 例外処理のスタック巻き戻しは、比較的遅いプロセスです。
- 逆に通常の実行ロジック部分のパフォーマンスが重要である。
- 通常の実行フローでは全ての関数のエラーチェックをする必要はなくなるので、 エラー処理に対して例外を使うことで、 現実に高速化します。
- 全てのエラーは何らかの方法で処理される。 明示的に書かれたエラー処理コードか、 あるいはシステムのデフォルト処理か。
- もし特定のエラーに対してハンドラが存在しなければ、ランタイムライブラリの デフォルトのハンドラで処理されます。仮にエラーが無視されるとすれば、 それはプログラマが明示的にエラーを無視するコードを書いたからで、 それは意図的な処理であると考えられます。
- エラー発生部分のコードの方が、エラーから復帰する部分のコードよりも エラーについてよく知っている。
- もはや、エラーコードを人間に読める形式に変換する必要はありません。 正しい文字列は、エラー対処コードではなく、エラー検知コードから生成されます。 これはまた、同じエラーに対しては異なるアプリケーション間で一貫性のある メッセージが表示されることにつながります。