REALbasic におけるより安全なコーディングについて

Index

イントロダクション

今朝のことになりますが(正確には、まだベッドに横たわっているときです)、私は 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

こうしたことは、例外を処理する上での基本です。

効果的な例外処理に関するさらなる提案と注釈

最後に

Anjo Krank 氏の建設的な意見に謝意を表します。

本稿に関して追補、修正、質問等がありましたら rb@tempel.org までお知らせ下さい。


Written and copyrighted by Thomas Tempelmann, March 14, 99; last edit: July 26, 99