前回は、strtok_getnext() の最中に、新たに strtok を使用することができない、という話をしました。
この原因を、わかりにくく言うと、strtokモジュールが1つしかないからです。
しかし、この当たり前を覆すちょっと便利な機能が、このページで扱う、「モジュール変数」機能です。
今回は「モジュール変数」機能の、概念と使い方のみの話となります。
作り方、文法的な説明は次回以降に行う予定です。
例えば、もし strtok モジュールが2つあったならば、それらを(1)「改行で区切るもの」と(2)「カンマ , で区切るもの」として、以下のように正しく処理できます。
// ※スクリプトはイメージです。 buf = {" りんご,100,30 みかん,170,12 ぶどう,1600,2 "} cntItem = 0 // 商品の種類数 // “1つ目の” strtok バッファに設定 strtok1_setBuffer buf, "\n" // すべての行について、1行ずつ処理 repeat if ( strtok1_isFinished() ) { break } data = strtok1_getnext() // 次の「行」 // “2つ目の”strtok バッファに設定 strtok2_setBuffer data, "," name(cnt) = strtok2_getnext() price(cnt) = int( strtok2_getnext() ) stock(cnt) = int( strtok2_getnext() ) cntItem ++ loop ...
前回の失敗は、「カンマ , で分割しようとするときに、strtok バッファが“上書き”されてしまう」というものでした。
しかし、上のイメージでは、strtok が2つある(= 別の変数を使っている)ので、“上書きされる”ということはありません。
これがHSPで書ければ、strtok の問題は解決するでしょう。
一番簡単(安易)な方法は、「strtok を2つ書く」というものです。
実際、前回の strtok モジュールを2つにコピーして、片方を strtok→strtok1 、もう片方を strtok→strtok2 と適当に名前を変えれば、上のスクリプトを動かすことができます。
しかし、この方法で3つ、4つと増やしていくのは現実的ではないでしょう。無駄で、面倒で、なにより美しくないです。
そこで登場するのが「モジュール変数」機能なのです。
ところで、「strtok モジュールが1つしかない」「strtok モジュールが2つある」などと書いてきましたが、そもそも「strtok が2つある」とは、どういう状況でしょうか?
全く同じことが書かれたモジュールが2つあっても、意味がないんじゃ……という気がしませんか?
答えは、次の2点です。
さて、HSPの「モジュール変数」機能では、次の2つのことができます。
ここでは、実際に作られた「変数のセット」のことを「インスタンス」(instance; 実体)と呼び、インスタンスを引数で受け取る命令・関数のことを「メンバ関数」(member function)と呼ぶことにします。
さらに、このように「インスタンス」を作ることのできるモジュールのことを、「クラスモジュール」(造語; class module)と呼びます。
では、モジュール変数機能をスクリプトの中で使う方法を説明します。
以降の例を実行するために、「モジュール変数」機能で書きなおした strtok モジュールをここに置いておきます。#includeしてお使いください。
+参照:「mcstrtok.as」
まず、「インスタンス」(変数のセット)を作成するには、newmodという命令を使います。
// 凡例 newmod (インスタンスを格納する変数), (モジュールクラス名), (コンストラクタ引数)...
newmod lineToker, strtok, buf, "\n"
ここでは strtok のインスタンスを新しく作り、変数 lineToker の中に置きました。
「コンストラクタ」(constructor)というのは、インスタンスが生み出されるときに呼ばれる命令で、その引数が「コンストラクタ引数」です。これは主にインスタンスの最初の状態(つまり各変数の初期値)を決めるために使われます。
strtok の場合、前回書いた strtok_setBuffer とほぼ同じ内容だと思ってください。そちらを再掲しておきます:
#deffunc strtok_setBuffer str sBuf, str delimiter
stt_buf = sBuf
stt_delimiter = delimiter
stt_index = 0
stt_lenBuf = strlen(stt_buf)
stt_lenDelimiter = strlen(stt_delimiter)
return
このインスタンス lineToker は「行を区切る strtok」として使いたいので、対象の文字列は buf で、区切り文字には "\n" を指定しています。
メンバ関数は、通常の命令・関数と同じように使えます。
異なるのは、最初の引数でインスタンスを受け取ることです。メンバ関数の中では、与えられたインスタンスの中にある変数を扱うことができるのです。
data = strtok_getnext( lineToker ) // lineToker を使って getnext の処理をする ; data に "りんご,100,30" が代入されたはず。
ここで各行のデータを「カンマ , ごとに区切る」ために、もう一つ strtok のインスタンスを作りましょう。
やり方は先ほどと同じです。
newmod dataToker, strtok, data, ","
変数 dataToker の中に、「カンマで区切る strtok」として使えるインスタンスを作りました。
このインスタンスは、変数 lineToner がもつインスタンス(改行で区切るstrtok)とは異なるので、前回のように“上書き”なる現象が発生することはありません。
// data = "りんご,100,30" name = strtok_getnext( dataToker ) // dataToker を使って getnext の処理をする ; name に "りんご" が代入されたはず。
加えて、「インスタンス」を破棄する命令 delmod を紹介しておきます。
「インスタンス」は、不要になった際に専用の“解体”処理を行う必要があります。
これは通常HSPによって自動で行われるので、気にする必要はありません。[脚注]
しかしたまに、インスタンスを特定のタイミングで破壊したいことがあります。そのときに用いるのが delmod 命令です。
delmod dataToker
これで、変数 dataToker の中身は“空っぽ”になります。空っぽの状態をnull(ヌル)といいます。
null の変数には注意してください。例えばこの後に、
name = strtok_getnext( dataToker )
などと書くと、strtok_getnext 関数が dataToker の中にある変数を使おうとするのですが、中に変数がないため、エラーになります (「モジュール変数の指定が無効です」)。
変数がインスタンスを持っているのか“空っぽ”(null)なのかは、varuse 関数を用いて調べます。
if ( varuse( dataToker ) == 0 ) { mes "dataToker は null です。" }
varuse が 0 を返したら“空っぽ”(null)、0 以外の値 (1 か 2) を返したらインスタンスが存在する、ということです。
ちなみに、「インスタンスを格納できる型」は struct 型といいますが、それ以外の型の変数に varuse を用いると、「サポートされていない機能を選択しました」というエラーが出てしまいます。
そのため、実際はさらに vartype 関数も用いて、変数が struct 型であることを調べておいた方が安全です。
if ( vartype( dataToker ) == vartype("struct") ) { mes "dataToker は struct 型です。" }
ちょっと脱線するというか、ある事情から、少し余計な話をします。
先に述べたように、「モジュール変数」機能は“複数個の存在”(multiple instances)という概念と密接に関係があるのですが……。
HSPにおいて、“複数個”を簡単に表現する手段が1つありましたね。配列です。
「モジュール変数」機能には、配列変数と絡めて使うことを前提としている仕様がいくつかあるので、この節ではそれらを紹介します。
まず、newmod 命令は、変数を自動的に配列として扱う仕様になっています。
どういうことかというと、次のように、同じ変数を2回 newmod すると、その変数は2つの要素を持つ配列になります。
// modcls という名前のモジュールクラスを定義 #module modcls m_ #global // 定義終わり newmod v, modcls // 新しいインスタンスが v(0) に入る newmod v, modcls // 新しいインスタンスが v(1) に入る mes length(v) //→ 2
もし配列の途中に“空っぽ”の要素があれば、そこに新しいインスタンスを作ります。省エネ仕様です。
#module modcls m_ #global newmod v, modcls // 新しいインスタンスが v(0) に入る newmod v, modcls // 新しいインスタンスが v(1) に入る delmod v(0) // v(0) を破棄して null にする newmod v, modcls // 新しいインスタンスは v(2) ではなく v(0) に入る
なお、この仕様は、「新しいインスタンス」がどの要素に入ったのか分からない、という問題があります。それを正しい方法で知るには、newmod を行う前に、“空っぽ”の要素があるかどうかを調べておくしかありません。[脚注]
「モジュール変数」配列の各要素に対して処理をするとき、repeat の代わりに foreach という命令が使用できます。
これは、単に配列の要素の数だけ反復するのではなく、“空っぽ”の要素の周を飛ばす、という機能がついています。
#module modcls m_ #global // 要素を3つ生成 repeat 3 newmod v, modcls loop // v(1) を破棄する delmod v(1) // 各要素についてループする foreach v mes "" + cnt + " : " + varuse( v(cnt) ) loop // v(1) = null なので、cnt = 1 のときは飛ばされている
さて、以上の話から、strtok のサンプルを「モジュール変数」機能に対応して書き直すことができます。
#include "mcstrtok.as" // 商品データ (という設定) // 各行は「商品名,単価,在庫数」ということにする buf = {" りんご,100,30 みかん,170,12 ぶどう,1600,2 "} // buf を strtok で分解する (改行で区切る) newmod lineToker, strtok, buf, "\n" // すべての行について、1行ずつ処理 repeat if ( strtok_isFinished( lineToker ) ) { break } // 一行取り出す data = strtok_getnext( lineToker ) if ( data == "" ) { continue } // data を分解する ( ',' で区切る ) newmod dataToker, strtok, data, "," name(cnt) = strtok_getnext(dataToker ) price(cnt) = int( strtok_getnext( dataToker ) ) stock(cnt) = int( strtok_getnext( dataToker ) ) delmod dataToker // 次の newmod に備えて破棄しておく cntItem ++ loop // 商品リストを再構成 repeat cntItem mes strf("「%s」は単価 %d 円で、%d 個の在庫があります。", name(cnt), price(cnt), stock(cnt)) loop stop
構造上「newmod dataToker」が repeat ループの中に入っています。つまり、ループの2回目では dataToker(1) にインスタンスが入ってしまいます。
しかし、インスタンスは1ループごとに1つ使うだけなので、dataToker を配列にする必要はありません。
そのため、ループの最後で毎回 delmod をしています。こうしておけば、次に newmod するときにも、インスタンスが dataToker(0) に入りますよね。
このために少し脱線をしました。
ちなみに dataToker を1つだけ作っておいて、ループの各周回で使い回す、という実装も確かにあります。しかしサンプルとして読みづらくなるのでやめました。作っては壊し、作っては壊し、という動きはややもったいないですが、まあぜいたくしましょう。
newmod で生成されたインスタンスは変数の中に入りますが、インスタンスを移動させる手段は、実はありません。
例えば次のように、代入文を使うと、あまり直感的ではない挙動になります。
#module modcls m_ #global // 変数 inst1, inst2 の中にインスタンスを作る newmod inst1, modcls newmod inst2, modcls // 変数 inst2 に変数 inst1 のインスタンスを「代入」……? inst2 = inst1 // 変数 x に inst1 のインスタンスを「代入」……? x = inst1
特に前者の代入文「inst2 = inst1」のたぐいはわりと危険です。inst2 の中身によっては、正常に解体されない可能性があるので。
後者の代入文「x = inst1」のたぐいは期待通り、「x と inst1 が同じインスタンスをもつ」ようになるようにみえます。しかし inst1 が delmod されたり、他の値を代入されたりすると、x も機能しなくなるので注意が必要です。
つまり、「モジュール変数」のインスタンスの代入文は使わないほうが無難です。
インスタンスの中にある変数(メンバ変数)の値は、標準の DebugWindow では表示されません。
拙作 knowbug なら、表示に対応しています。
+参照:knowbug
repeat length( (配列) )
」の代わりにも使える。スキップはされない。
さて、今回は「モジュール変数」機能を「使う」側の説明をしました。
次回は、「モジュール変数」機能で「作る」側の話になります。
質問などあれば掲示板までどうぞ。