今朝のことになりますが(正確には、まだベッドに横たわっているときです)、私は REALbasic での例外処理がなんと簡単かつ素晴らしいやり方で可能であるのかに思い至りました。(私は既に基本的なやり方は知っていましたが、 Exception はクラスとしての性質を持っているのだから拡張可能であることに電撃的に気がつき、そして本稿を書くために飛び起きて、Mac の前に急ぎました)
例外(エラー) 処理に関しては REALbasic 内には殆ど文章化されていません。わずかに文法の説明として与えられているだけで、具体的な例や何の役に立つのかの説明はないのです。
そこで、オブジェクト指向やモダンなプログラミング技術に初めて接する全ての REALbasic ユーザの為のささやかなチュートリアルを、本稿において提供することにします。
Copyright に関する注意: 再配布に関しては、それが無料で行われる限り、これを許可いたします。著作権は私が保持するものとします。Thomas Tempelmann(rb@tempel.org).
例外処理は、何らかのエラーによって関数の実行を抜け出したい場合いつでも役に立ちます。
ファイルに、テキストと数字(常に正であると仮定)の行を追加する関数を考えてみましょう。基本的なコードは以下のようなものでしょう。
Sub AddLineToTextfile(text as String, posNum as Integer, fileRef as FolderItem) Dim fileStream As TextOutputStream fileStream = fileRef.AppendToTextFile fileStream.WriteLine (text + " (the number is: " + Str(posNum) + ")") fileStream.Close End Sub
この関数を使うコードは以下のようなものです。
Sub WriteSomeLinesToAFile() Dim tmpFileRef as FolderItem tmpFileRef = GetFolderItem("tmpFile.txt") if tmpFileRef.Exists then mpFileRef.Delete() end AddLineToTextfile("2*2", 2*2, tmpFileRef) AddLineToTextfile("5*5", 5*5, tmpFileRef) End Sub
AddLineToTextfile 関数の問題点は、その呼び出し元が引数を正しい形で渡してくれていると仮定していることにあります。
呼び出し元が守らなければならない仕様として、いくつか考えられます。
こうした仕様を知っている限りは、関数呼び出し前にこれらを確かめることが出来ます。
例えば、後になって AddLineToTextfile 関数を別のプロジェクトで再利用する場合や、この関数を一般に公開した場合には、関数が適切に呼び出されているかをあらかじめ確かめたいでしょう。
さて、どうやれば確認できるでしょうか?
関数でエラーの発生を察知し、それを処理したい場合の伝統的なやり方は次のようなものです。
// 渡された値が正かどうかを確認する IF posNum < 0 THEN // おっと、エラーだ! RETURN END IF fileRef = NIL THEN // 別のエラー! RETURN END // ... 通常の処理を行う
コード中に不適切な値の場合の処理を書いてしまうと、呼び出し側に引数が仕様を満たしていないことを伝えないことになります。
呼び出し側に処理が正常に行われたか否かを示す値を返すやり方も出来ます。しかし、その場合呼び出し側は、追加の返値を受け取るコードだけでなく、その値をチェックしてそれに応じた処理を行うコードも追加する必要が生じます。
あるいは、MsgBox() 関数を使って、エラーであることを簡単なシグナルで表示する以下のようなやり方も考えられます。
IF posNum < 0 THEN MsgBox "AddLineToTextfile 実行中にエラーが発生: posNum < 0!" RETURN END
しかし、これは下に挙げる理由により、お勧めできません。
例外を発生させるやり方では、これら全ての問題を解決することが出来ます。これには、次のようにコーディングしてやります。
IF posNum < 0 THEN Raise new RuntimeException END IF fileRef = NIL THEN Raise new RuntimeException END
例外を発生させた場合、それは次のように作用します。第一に、あたかも RETURN ステートメントを使ったかのように振る舞い、その関数の実行が終了します。次に、呼び出し側がその例外の処理を明確に行っていない場合には、(まるで成功値が返ってきたかのように)処理が続けられますが、しかし呼び出し側関数のさらに呼び出し元に渡され、さらにその呼び出し元に渡され・・・、というようにその例外が処理されるまで外側の関数に渡されていきます。もしもどの関数も例外を明確に処理しなかった場合は、REALbasic が標準エラーダイアログを呼び出し、アプリケーションの実行を停止してエラー状況を表示します。
例外を処理するために、いわゆる例外ハンドラを関数の最後に追加します。
Sub WriteSomeLinesToAFile() ... Exception exc as RuntimeException MsgBox "何か問題が生じました" End Sub
上に見られる例外の捕獲は、アプリケーションにおけるあらゆる例外の捕獲の、もっとも一般的な方法です。すなわち、存在しないオブジェクトへのアクセス(オブジェクトが NIL である場合に生じる)、スタックオーバーフロー(サブルーチンのネストが深すぎる場合、特に再起呼び出しの際に生じる)、配列の添え字エラー(添え字の範囲超過の場合に生じる)、といった例外です。
こうしたコードを全てのイベントハンドラルーチンに追加することで、アプリケーション中のあらゆるエラーをトラップすることが可能になり、例外が生じた場合に自動的に終了してしまう代わりに、なんらかの処理を記述することが出来るようになります。
"RuntimeException" を発生させるのは包括的な方法で、呼び出し側に特定のメッセージを送ることは出来ません。もし関数が、エラーの状況の詳細を示すテキストやエラー状況と関連したパラメータ(例えばファイル名や、正でない値など) といった、もっと多くの情報をつめこんだ例外を発生させることができればより素晴らしいでしょう。
そのために、"RuntimeException" クラスの派生クラスを作る必要があります。REALbasic のエディタ中で新しいクラスを作り、"MySpecialException" と名付けてその親クラスを RuntimeException にセットしてください。
これにより、RuntimeException を発生させる代わりに、独自の例外を発生させることが可能です。
IF posNum < 0 THEN Raise new MySpecialException END
例外ハンドラでは、これを次のように使います。
Exception exc as RuntimeException IF exc isA MySpecialException THEN MsgBox "AddLineToTextfile の実行に失敗しました" ELSE MsgBox "何か問題が生じました" END End Sub
ここで、WriteSomeLinesToAFile 関数内では、発生してくる未知の例外を全部処理したいというわけではなく、(AddLineToTextfileで発生する)一部の例外のみに注目したいわけで、そういう目的のために、(関係ない)例外を上の階層の呼び出し元関数にそのまま通してやることが出来ます。
Exception exc as RuntimeException IF exc isA MySpecialException THEN MsgBox "AddLineToTextfile の実行に失敗しました" ELSE // ここで処理したくないその他の例外 Raise exc END End Sub
もっと短く記述するなら、上のコードは下のように出来ます。
Exception exc as MySpecialException MsgBox "AddLineToTextfile の実行に失敗しました" End Sub
(これは MySpecialException 型の例外のみが処理され、その他全ては上位の階層に渡されることを意味します)
オブジェクト指向プログラミングの概念と、例外がオブジェクトであるという事実を生かして、例外をシグナル化することで、さらにスマートかつ一般的な形としてメリットを享受できます。
REALbasic のエディタで、MySpecialException クラスにプロパティを追加します。
Add the property: msg as String
ここで、この型の例外を発生した際に、以下のように msg プロパティになんらかの情報を追加することが出来ます。
Dim myExc as MySpecialException ... IF fileRef = nil THEN myExc = new MySpecialException myExc.msg = "fileRef = nil (posNum = " + Str(posNum) + ")" Raise myExc END IF posNum < 0 THEN myExc = new MySpecialException myExc.msg = "file " + fileRef.name + ": posNum < 0" Raise myExc END
例外ハンドラでは、このプロパティを利用することが可能になります。
Exception exc as MySpecialException IF exc.msg = "" THEN MsgBox "未知の理由により AddLineToTextfile の実行に失敗しました" ELSE MsgBox "以下の理由により AddLineToTextfile の実行に失敗しました: " + exc.msg END End Sub
例外ハンドラをきちんと処理するためには、別の問題もあります。サブルーチンにおいて、検出しえない、したがって適切な処理や予防をしえない多くのエラー状態があり得ます。
しかし、最低限行わなければならないことは、そういった例外的なケースに注意し、サブルーチンの中で操作するデータの "クリーンアップ" を行うことです。
例えば、サブルーチン中でファイルを開いて書き込む場合、スタックオーバーフローのような予測不可能なエラーが生じたならば、少なくとも参照をなくしてしまう前にファイルを閉じておかねばなりません。
以下がその例です。
Sub AppendSomeDataToFile (fileRef as FolderItem) Dim fileStream As TextOutputStream Dim needsClose as Boolean fileStream = fileRef.AppendToTextFile needsClose = true fileStream.WriteLine ("just some data") needsClose = false fileStream.Close Exception exc as RuntimeException // 何らかのエラーが生じたらクリーンアップを行う // この場合、ファイルが開いていたら閉じる IF needsClose THEN fileStream.Close END // 例外を渡す Raise exc End Sub
こうしたことは、例外を処理する上での基本です。
コールバックルーチン(Socket.Error など) で例外を発生させる場合には注意が必要です。不適切なスレッドに投げられることがあり、この場合捕獲されないことになります。
ひとつのサブルーチンで複数の例外型をテストしたい場合、IsA オペレータを使うかわりに、次のようなやり方でやります。
Sub Y DoSomeThingBad ... Exception x as XExc DoSomethingToFixX Exception y as YExc DoSomethingToFixY End Sub
REALbasic version 2 では、いわゆるコンストラクタを使って、より簡単に例外ハンドラに引数を渡すことが出来ます。MySpecialException 中でこのメソッドを追加します。
Sub MySpecialException(msg as String) self.msg = msg End Sub
そして、次のように 1行でもっと簡単にメッセージ付きの例外を発生させることが出来ます。
Raise new MySpecialException ("here comes the message")
REALbasic version 2 では Variant と呼ばれる新しい型も導入されました。これを使うと、String を Variant に変更することで、メッセージをより多目的に使用可能になります。Strings, Integers, Booleans などといったあらゆる基本型を渡せるようになるのです。
Anjo Krank 氏の建設的な意見に謝意を表します。
本稿に関して追補、修正、質問等がありましたら rb@tempel.org までお知らせ下さい。