これまでのTips
はじめに
今回は、Curl言語の中でも特徴的なnull値の取り扱いについてご説明したいと思います。
null値の扱い
プログラマの方であれば、NullDereferenceException(NullPointerException)は、かなりなじみのある例外エラーだと思います。変数にインスタンスが設定されていない、すなわちnull値が入っている状態で、プロパティを設定したり、メソッドを呼んだりすることで発生します。
Java等の言語では、変数にnull値が入ることを想定してプログラムを書くか否かはプログラムを作成した人次第であり、引数などからそれを判断することができません。そこで、提供されたクラスライブラリなどを使う場合、プロシージャやメソッドの引数にnull値を許すかどうかを、APIのドキュメントを頼りに判断することになります。多くの場合、実行してみなければどのような結果になるのか分かりません。
それに対してCurl言語は、変数にnull値を許すか否かを明示する仕様になっているため、引数の型を見るだけでnull値を渡せるかどうか判断することができます。これにより、検証用のコードを書かなくて済みますし、null値によるExceptionの発生をかなり減らすことができます。その代わり、自分がコードを書く場合に、常にnull値をとるのかとらないのかを明確に意識する必要があります。
Curl言語は、null値の扱いについて、いくつかのマクロが提供されています。これらのマクロを使うことで効率的にコードを書くことができます。
具体的に見ていきましょう。null値をとる変数型は、先頭に#を付けます。文字列クラスを例にしますと、null値をとらない文字列クラスはString型、null値をとる文字列クラスは#String型になります。文字数を数えるプロシージャの引数として、この2つの変数を渡してみます。
{curl 7.0 applet} {curl-file-attributes character-encoding = "shift-jis"} {applet manifest = "../manifest.mcurl", {compiler-directives careful? = true} } || 文字数を返すプロシージャ {define-proc public {length str:String}:int {return str.size} } || 呼び出し可能 str0の文字数: {value let str0:String = "abc" {length str0} } || エラー発生 str1の文字数: {value let str1:#String = "abc" {length str1} || エラー }
プロシージャの引数には、String型を指定しています。String型の変数を引数にプロシージャを呼び出すことができますが、null値を含む#String型の変数を渡すと、図1のようにコンパイルエラーとなります。コンパイラはString型と#String型を別の型として処理しているためです。
asa演算子
String型の引数に#String型の変数を引き渡すには、asa演算子を使って#String型をString型に変換(キャスト)します。asa演算子を使って明示的なキャストをすることで先ほどのプロシージャを実行できます。
{curl 7.0 applet} {curl-file-attributes character-encoding = "shift-jis"} || 文字数を返すプロシージャ {define-proc public {length str:String}:int {return str.size} } || 実行可能 strの文字数: {value let str:#String = "abc" {length str asa String} || Stringへのキャスト }
asa演算子を使ってキャストを行った場合、変数にnullが入っていますとExceptionが発生します。asa演算子を使うことで問題なのが、クラス名を記述しなければならないことです。String型の場合はクラス名が短いので、"asa String"を付けてもさほど手間ではありません。しかし、ハッシュテーブルのような長いクラス名の引数を呼び出すような場合や、変数のクラス名を途中で変更した場合など、"asaクラス名"をソースコードに記述していくのは、コードの記述量が増えてしまいますし、メンテナンス性も下がります。
そのような場合、asa演算子を使ってキャストする代わりにnon-nullマクロを使います。
non-nullマクロ
先ほどの例をnon-nullマクロを使って書き直してみます。
{curl 7.0 applet} {curl-file-attributes character-encoding = "shift-jis"} || 文字数を返すプロシージャ {define-proc public {length str:String}:int {return str.size} } strの文字数: {value let str:#String = "abc" {length {non-null str}} || non-nullマクロ }
non-nullマクロは、#付の変数を#無しの変数として処理します。型名を書かなくて済むなど、コードの変更に対しても柔軟に対応できます。
if-non-nullマクロ
次は、null値を許す変数でnull値の判定をするif文です。
null値を許す変数やフィールドに対して、null値の場合とそうでない場合で処理を分けて行うことはよくあります。先ほどの文字数をカウントするプロシージャを使って、null値であれば0を、null値でなければ文字数を設定してみましょう。コードは以下のようになります。
{curl 7.0 applet} {curl-file-attributes character-encoding = "shift-jis"} || 文字数を返すプロシージャ {define-proc public {length str:String}:int {return str.size} } strの文字数: {value let str:#String = "abc" {if str != null then {length {non-null str}} else 0 } }
このようなケースでは、if-non-nullマクロが便利です。if-non-nullマクロで書き直してみます。
{curl 7.0 applet} {curl-file-attributes character-encoding = "shift-jis"} || 文字数を返すプロシージャ {define-proc public {length str:String}:int {return str.size} } strの文字数: {value let str1:#String = "abc" {if-non-null str then {length str} || non-nullが不要 else 0 } }
if-non-null thenの次の{length str}のところに注目してください。null値を許す変数strを直接呼び出しているように見えます。if-non-nullマクロは、変数strをnull値を許さないString型として定義します。ifコードブロック内にstrというローカル変数(null値を許さないString型)を定義して、利用していると考えられます。
このようなコードが書けるのは、strがローカル変数の場合のみです。それ以外の場合は、別途if-non-nullのコードブロック内で利用する変数を指定します。変数を指定することで、フィールドやプロシージャの戻り値を使うことができます。
{curl 7.0 applet} {curl-file-attributes character-encoding = "shift-jis"} || 文字数を返すプロシージャ {define-proc public {length str:String}:int {return str.size} } || 文字をそのまま返すプロシージャ {define-proc public {mirror str:#String}:#String {return str} } strの文字数: {value let str:#String = "abc" {if-non-null str1 = {mirror str} then {length str1} else 0 } }
if-non-nullで導入された変数str1は、型の定義を行っていませんが、mirrorプロシージャの戻り値#Stringのnull値を許さない型であるStringが自動的に定義されます。このように、if-non-nullマクロはとても便利です。
uninitialized-value-for-typeマクロ
クラスを定義した場合、フィールドの型にnull値を許すか否かについては、結構悩むところです。null値を許さないフィールドの方が利用する場合に便利なのですが、初期値としてなんらかの値を与えておく必要があります。
変数に初期値は設定できない(しにくい)けれど、非null値の変数を定義したい場合、uninitialized-value-for-typeマクロを使うことで、null値を許さない変数に対して一時的にインスタンスが未設定の状態を作り出すことができます。実際にクラスを作成し、フィールドで利用している例を示します。
{curl 7.0 applet} {curl-file-attributes character-encoding = "shift-jis"} || クラス {define-class public Child field public name:String = {uninitialized-value-for-type String} {getter public {pet-name}:String {return self.name & "ちゃん"} } } {value let a:Child = {Child} let b:Child = {Child} set a.name = "さり" set b.name = "まい" a.pet-name & "と" & b.pet-name & "はお友達" }
実行した結果(図2)です。
uninitialized-value-for-typeマクロを使った変数には、どのような値が入っているのでしょうか? デバッガで変数aのインスタンスを生成した所で止めて、nameフィールドの値を見ます(図3)。
uninitialized-value-for-typeマクロを使ったnameフィールドの値は「null」でした。このマクロは、コンパイラを一時的にだましていることになります。フィールドを読み出す前に値が設定されることが確実な時だけ利用しましょう。値が設定されていないのに値を読み出せば、当然Exceptionが発生することになります。
まとめ
最後に今まで説明してきたマクロを実際に利用するサンプルを見ていきましょう。1人の子供をあらわすChildクラスを定義しています。コンストラクタで名前を指定し、set-friendメソッドでお友達の名前を追加していきます。
{curl 7.0 applet} {curl-file-attributes character-encoding = "shift-jis"} || こどもクラス {define-class public Child || 名前フィールド field public name:String = {uninitialized-value-for-type String} || コンストラクタ {constructor public {default name:String} set self.name = name } || 友達リスト field private _friend-list:#{Array-of String} = null || ゲッターでインスタンスを生成 {getter public {friend-list}:{Array-of String} {if self._friend-list == null then set self._friend-list = {{Array-of String}} } {return {non-null self._friend-list}} } || 友達を設定する {method public {set-friend name:String}:void {self.friend-list.append name} } || 友達全員の文字列を返す {getter public {size}:int {return {if-non-null friend-list = self._friend-list then friend-list.size else 0 } } } } {value let mai:Child = {Child "まいちゃん"} {mai.set-friend "まなちゃん"} {mai.set-friend "としくん"} {mai.set-friend "かっちゃん"} {mai.set-friend "みーちゃん"} {format "%sの友達は%d人です。", mai.name, mai.size} }
実行結果です(図4)。
Childクラスのnameフィールドは、{uninitialized-value-for-type String}により定義されており、コンストラクタで値が入ります。よって、#Stringにする必要がありません。
友達のリストを表す _friend-list フィールドは、#{Array-of String}となっており、友達リストが使われた時に初めてインスタンスを生成しています。インスタンスを遅延生成することで、Childクラスの生成を高速化できます。ここでは、フィールドは、#{Array-of String}ですが、ゲッターを {Array-of Stirng}にすることで、利用する時にnull値判定が不要になり使い勝手が良くなります。
最後に
Curl言語の特徴的な機能である、null値の取り扱いについて見てきました。曖昧なnull値の利用を排除することで質の高いプログラムを書くことが可能です。マクロを利用することで、他の言語と比べても遜色なく効率よくコードを書くことができます。マクロの1つ1つはとても簡単な内容ですので、ぜひ利用してみてください。