ジュリアン・モニペニー著 コードを書くこと自体はたいがい簡単で、時間がかかるのはいつもエラー処理の部分です。エラーはいろいろな形や規模で発生しますが現実には爆発性または放射性のいずれかです。爆発性のエラーは一目でわかります。プログラムが実行に失敗したと例外メッセージが知らせてくれるからです。爆発性のエラーは最初は恐いかもしれませんが、解決するのはそれほど難しくありません。頭を冷やして注意深く分析すれば、プログラムを制御しながら再起動させることができます。これに対し放射性のエラーはずっと厄介です。プログラムは実行を続けますが、その出力は変異します。腐敗の根源を突き止めるまで数週間、数ヶ月かかることもあり、損害の修復は悪夢のような作業になることもあります。 エラーがどのようなものであっても、予防に優る治療はありません。開発中にエラーを修正するのに比べて、運用中にエラーを修復するほうが100倍コストがかかる場合もあります。本稿では、フィードバック組み込み関数(BIF: Built-In Function)、防御的コーディング、エラー監視、終了手続きという4つのエラー処理テクニックについて述べます。これらのテクニックを理解しておくと、プログラムのエラーを根絶するのに役立ちます。エラーの抑制、エラーからの回復、プログラムの異常終了についてお教えしましょう。 フィードバックBIF フィードバックBIFを使用すると、命令が正常に実行されたかどうかを知らせてくれます。フリー・フォーマットのRPGで使用する最も一般的なフィードバックBIFを、図1に示します。図1のBIFはすべて*Onまたは*Offをという値を返します。BIFは通常入出力操作に使用します。 関連する任意の操作の後に%EOFや%Foundをコーディングします。Chain操作でレコードが見つからなかったことをチェックするには、次のようにコーディングします。 chain key file; if not %found( file ); // Handle not found condition endif; %EOFおよび%FoundのBIFはともにオプションで引数を指定することができ、そこでフィードバック元を定義します。引数を指定しないと、BIFは引数として指定可能な最後の操作を参照します。つまり以下の%FoundのBIFは、file1ではなくfile2を参照します。 chain key1 file1; chain key2 file2; if not %found( ); %ErrorのBIFは、エラー拡張子を指定した操作の後にだけコーディングすることができます。Write操作で発生したエラーをチェックするには、次のようにコーディングします。 write(e) format; if %error(); // Handle error endif; %EOFや%Foundとは異なり、%Errorはフィードバック元を定義する引数を指定しません。%ErrorのBIFが参照する操作のすぐ後に、%Errorをコーディングします。%Errorはデータをあるタイプから別のタイプに変換させる前に、その値が正当かどうかをテストする際に使用すると特に有用です。次のコードでは、整数フィールド中の*ISO日付を日付タイプに変換する前に値をチェックします。 test(de) *iso int; if not %error(); date = %date( int: *iso ); endif; このコードを実行すると、%DateのBIFで爆発性エラーが発生して停止します。しかしフィードバックBIFは通常は放射性エラーで停止します。Chain操作やRead操作でエラーを発見できなかったということは、入力フィールドは最後に成功した入出力操作で得た値をそのまま保持しているということになります。これではエラー処理を行うのではなく、嘘の値を再利用することになってしまいます。 防御的コーディング 防御的コーディングとは、エラーの発生を未然に防いだりあるいは少なくともエラーが発生したときに手際よく処理したりするためのコーディングです。エラーを防ぐようなコードでほとんどの操作を「保護」しておくべきです。こう言うと怖気づいてしまうかもしれませんが、図2に示す簡単なチェックリストでほとんどのエラーをカバーできます。「保護」コードは、たいていの場合は境界条件を調べるだけです。 配列のインデックスのエラーを防ぐには、%ElemのBIFを使用して配列のインデックスが配列中の要素を参照しているか否かをチェックします。 if idx >= 1 and idx <= %elem( array ); elem = array( idx ); endif; もちろん、インデックスの境界をテストしたからといって、そのインデックスで参照した配列の要素に正しいデータが含まれているという保証はない、ということを忘れないでください。 文字列内の位置についてのエラーを防ぐには、%LenのBIFを使用して文字列の開始位置が正しいか否かを調べます。 if pos >= 1 and pos <= %len( string ); substring = %subst( string: pos ); endif; 0による除算を防ぐには、除数がゼロでないことを調べるだけです。 if y <> 0; z = x / y; endif; 標準のデータ定義を使用してフィールドのサイズを確実に決めておくことで数字の丸めを防ぐことができます。 (p. 3) d x s like( stdInt ) d y s like( stdInt ) y = x; 防御的コーディングはたいていの爆発性エラーを防いでくれます。このセクションで紹介した例はいずれも、保護されていないと例外メッセージが出力される結果となります。防御的コーディングを使用して、予知可能な単純なエラーを回避してください。 エラー監視 ここまでは、BIFの使用や防御的コーディングにより単純なエラーを防いだり処理したりする方法について説明しました。しかしながらエラーは単純なものばかりではありません。たとえば次のような簡単な例を見てみましょう。 z = (a - b) / (c - d); 2つの値の差が問題となります。 if (c - d) <> 0; z = (a - b) / (c - d); endif; しかし、このやり方では理想的とはいえません。If文と次の代入文の半分が同じことを繰り返すことになるからです。これは式がもっと複雑になるとパフォーマンス、可読性、保守性に関係してきます。それでも上の解は安定した状況であれば受け入れることができるかもしれませんが、安定していないのです。たとえば(a b)がとても大きな値で(c d)がとても小さな値だったらどうでしょう。数字の丸めが発生するかもしれません。すると防御的コーディングではまったく要求を満たしていることになりません。常識が必要とされるのはこの部分です。エラーの発生をリモートからだけ監視できるのであれば、エラーを監視して修正するのです。 monitor; z = (a - b) / (c - d); on-error; z = 0; endmon; この例では、監視グループを使用してエラーをチェックしています。監視グループは、Monitorと最初のOn-Error操作との間の操作で発生するエラーをチェックします。エラーが発生すると、関連したOn-Errorブロックのコードに制御が渡されます。すべての操作が正常に終了すると、制御はEndMon操作に渡されます。上記の例の場合、エラーが発生した場合には監視ブロックがzに0をセットします。 監視グループには、前述の例に示したようにcatchall On-Errorの他にも特定のエラー・ステータスをチェックする複数のOn-Error文を含めることができます。次の例では、0による除算(00102)と丸め(00103)を明示的に監視しています。 monitor; z = (a - b) / (c - d); on-error 00102; // Handle divide by zero on-error 00103; // Handle truncation on-error; // Handle other errors endmon; On-Error文では、コロンを使ったり以下の予約語を使うことにより、複数のステータスを指定することができます。 ・*Programで00100から00999までのプログラム・ステータスをトラップ ・*Fileで01000から09999までのファイル・ステータスをトラップ ・ *Allでプログラム・ステータスとファイル・ステータスを両方トラップ 00001から00099までのステータスは監視できない点に注意してください。(ステータスの全リストについては、publib.boulder.ibm.com/iseries/v5r2/ic2924/books/c0925084.pdfに掲載のILE RPG Referenceマニュアル第5章の「ファイルおよびプログラムの例外/エラー」を参照してください。) 監視グループを入れ子にすることができます。入れ子になった監視グループの中でエラーが発生する場合、エラーをトラップできる最も内側のグループに制御が渡されます。監視グループは別のサブルーチンを実行中に発生したエラーをトラップすることはできません。しかし、呼び出したプロシージャやプログラムがそれ自身の中で発生したエラーをトラップしていなくても、ステータス00202で終了すれば発生したエラーをトラップすることができます。その際、呼び出したプログラムやプロシージャは実行に失敗します。あるプロシージャがエラーを監視している場合、On-Error文に制御が渡されます。プロシージャに監視が含まれていない場合、呼び出したプロシージャにある関連したOn-Error文に制御が渡されます。 同一のあるいは入れ子になったプロシージャ内にある入れ子になった監視グループを使用すると、コードが複雑になり、複雑になるだけでエラーが入り込む余地が生じます。アドバイスとして言えるのは、なるべくシンプルにすることです。各プロシージャがそれ自身のエラー回復を制御するものと考えてください。 エラー回復が完全で意図した通りの結果となっていることを確認してください。次の例を見てみましょう。 monitor; a = x; b = y; c = z; on-error; // Handle errors endmon; このコードではどのようにエラーを処理すべきでしょうか。エラーは3つの代入文いずれでも発生する可能性があります。たとえば、このコードがループ内で実行されるとしましょう。2回目のサイクルで、2番目の代入文でエラーが発生しました。Aが代入され、Bは失敗しました。Cでは依然として1回目のサイクルの値を保持しています。 エラーを処理する1つの方法は、すべてをクリアするというものです。 monitor; a = x; b = y; c = z; on-error; a = 0; b = 0; c = 0; endmon; この方法は、AとBとCが互いに依存していて1つのフィールドでエラーが発生すると3つの値すべてが無効になってしまうという場合にはうまくいきます。なお、これ以外にも2つの方法があります。まず1つ目は、代入前にすべてのフィールドをクリアしておいて、監視グループで例外をトラップするという方法です。 a = 0; b = 0; c = 0; monitor; a = x; b = y; c = z; on-error; // Do nothing endmon; この方法は、エラーが発生するまでに変数を割り当てたいという場合にはうまくいきます。 もう一つの方法は、次のように監視グループを3つの単純なグループに分割するという方法です。 monitor; a = x; on-error; a = 0; endmon; このテクニックは、エラー発生の有無にかかわらず、3つの代入をすべて行いたいという場合にはうまくいきます。 単純なコード・ブロックでさえも、エラー処理にはたくさんの方法がありますが、コードの目的を反映させたものが正しい方法といえます。どんな目的であれ、必要に応じてエラーを見つけ出して回復させるようなエラー処理であることを確認してください。特に、エラーを隠すようなことはしないでください。 monitor; a = x; on-error; // Do nothing endmon; このコードは、爆発的なエラーを放射的なエラーにしており、事態を悪化させています。 以上に示した代入文のエラーの例は、いずれもデフォルト値(0)を割り当てることによりエラーから回復しています。アプリケーションを中断することなく継続して実行できるような形でエラーを回復できるほうが望ましいのは、もっともなことです。しかし、場合によっては、重大なエラーであるためにそのエラーを明らかにしなければならないこともあります。だからといってプログラムを停止しなければならないというわけではありません。エラーを画面上やレポート形式で表示したり、ログファイルに残したりして回復し、実行を継続すれば十分な場合がほとんどです。 この手法をとる際には、エラーの詳細をトラップする必要があります。プログラム・エラーに関しては、図3に示すように、そのプログラムのプログラム・ステータス・データ構造中を見ればエラーの詳細がわかります。入出力エラーに関しては、入出力の対象となっているファイルのファイル情報データ構造を見ればエラーの詳細がわかります(図4)。両構造ともサブフィールドを持っており、そこでエラー・ステータスとそれに対応したメッセージIDおよびその他の関連データが定義されています。関連するエラーをトラップするには、On-Error操作で予約語*Programおよび*Fileを使用します。 (p. 6) monitor; // Do something on-error *program; status = program.status; on-error *file; status = fileInfo.status; endmon; プログラム・ステータス・データ構造およびファイル情報データ構造の詳細については、マニュアルILE RPG Referenceの第5章を参照してください。 終了手続き プログラムが異常終了する理由はさまざまです。監視されていないエラーは例外の原因となりますし、対話セッションが異常終了したり、ユーザーがシステム要求オプション2でプログラムをキャンセルしたり、ユーザーがジョブを終了したなどさまざまです。ILEは異常終了のさまざまな形を統一した処理方法で処理します。呼び出しスタック・エントリが呼び出し側に戻らずに終了したときに実行される呼び出しスタック・エントリ終了ユーザー出口手続き(略して終了手続き)がCEETRX APIで登録されます。終了手続きにより異常終了後の後始末を制御された方法で行うことができます。 ExitProcプログラム(図5)に終了手続きの使い方が示されています。このプログラムにパラメータとして「1」を渡して呼び出して例外を生成させるか、パラメータとして「2」を渡して呼び出し、システム要求オプション2を選択してプログラムの実行をキャンセルします。いずれの場合も「CleanUp実行」というメッセージが表示され、プログラムの終了時に終了手続きが実行されたことを示します。 ExitProcプログラムは終了手続きとして実行されるcleanUp手続き(図5中のA)をプロトタイプします。setExitProcedureプロトタイプ(B)はCEETRX API用のパラメータを定義し、これが呼び出されて(C)、 cleanUpを登録します。システムはプログラムが異常終了するとcleanUp手続きを呼び出します(D)。 終了手続きには好きなコードを記述することができます。簡潔にポイントをついて記述するのがポイントです。不必要なコードを記述して終了手続きの中で例外を引き起こすような危険を冒さないでください。通常は、ファイルのクローズ、ユーザー空間などの一時オブジェクトの削除、データのバッチを処理するジョブの実行などを行います。終了手続きのコードは監視ブロックの内側に保護しておくのが良いでしょう。終了手続きの詳細については、publib.boulder.ibm.com/iseries/v5r2/ic2924/info/apis/api.htmに記載されているILE CEE APIドキュメントの「アクティベーション・グループと制御フロー用API」の章を参照してください。 あなたの番です RPGにはエラーを処理するための方法が無数にあります。特にフィードバックBIF、防御的コーディング・テクニック、エラー監視、終了手続きを使用すると、エラーを制御するための安定した基礎を築くことができます。その基礎をどのように築くかはあなた次第ですし、記述しているコードの構造や目的にも依存します。エラー処理はシンプルにしてください。標準的な方法を使用して、アプリケーションのアーキテクチャを強化してください。エラーはできるだけ早い段階で食い止めるようにしましょう。フィードバックBIFや防御的コードを使用して、まずは予防を優先しましょう。実行の失敗を防ぐには、エラーからの回復の基本としてエラー監視を使います。そして最後の手段としては、終了手続きにより、実行が失敗し異常終了した際の後処理を制御された方法で行います。 ジュリアン・モニペニーはベル・データ鰍フ提携先でiSeries NEWS誌を発行しているPenton Media, Inc.のテクニカル・エディタです。