1. トップ
  2. HSP講座
  3. プリプロセッサ命令編
  4. >ここ [
    1. ←前
    2. |
    3. 次→
    ]

モジュールクラス・ユージング

前回は、strtok_getnext() の最中に、新たに strtok を使用することができない、という話をしました。
この原因を、わかりにくく言うと、strtokモジュールが1つしかないからです。
しかし、この当たり前を覆すちょっと便利な機能が、このページで扱う、「モジュール変数」機能です。

今回は「モジュール変数」機能の、概念と使い方のみの話となります。
作り方、文法的な説明は次回以降に行う予定です。

  1. 増加;Multiplication
  2. 相異なる2つの……
  3. よろしくインスタンス
  4. 「皮算用は計算練習として有用だ。」
  5. 類は友を呼ぶ
  6. 始まりの終わり
  7. 注意:インスタンスの代入
  8. まとめ
  9. おわりに

増加;Multiplication

例えば、もし 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つと増やしていくのは現実的ではないでしょう。無駄で、面倒で、なにより美しくないです。
そこで登場するのが「モジュール変数」機能なのです。


相異なる2つの……

ところで、「strtok モジュールが1つしかない」「strtok モジュールが2つある」などと書いてきましたが、そもそも「strtok が2つある」とは、どういう状況でしょうか?
全く同じことが書かれたモジュールが2つあっても、意味がないんじゃ……という気がしませんか?

答えは、次の2点です。

図で示すと次のようになります。
strtok モジュールが2つあるの図
※あえて前回の図を流用した。
このように、変数 stt_buf, stt_index, ... が、名前は同じでも、それぞれ2つずつになっていることが分かります。
また、命令・関数 strtok_setBuffer, strtok_getnext(), ... などは、処理の内容は同じですが、その中で使用する変数が異なるので、実質的に異なる命令・関数となっています。


よろしくインスタンス

さて、HSPの「モジュール変数」機能では、次の2つのことができます。

図で示すと次のようになります。
strtok「モジュール変数」機能のイメージ
※イメージ。
命令・関数が複数あっても、違うのは「どの変数を用いるか?」という点だけでしたから、用いる変数を引数で与えて決めることにすれば[脚注]、命令・関数は1つだけで済みます。
そういうわけで、「モジュールを複数作る」ことは、「モジュールの中の変数たちを複数作る」ことで実現することができます。

ここでは、実際に作られた「変数のセット」のことを「インスタンス(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


まとめ

クラスモジュール (class module)
この単語は覚えなくていい。しかし、普通のモジュールと、インスタンス化可能な(newmodを使える)モジュールは別物だと認識する必要はある。
メンバ関数 (member function)
「モジュール変数」機能に対応したモジュールが持つ命令や関数のこと。最初の引数でインスタンスを受け取り、インスタンスの中の変数を利用して処理をする。
命令のほうは「メンバ命令」(member statement)と呼んでもいいかも。
「メソッド」(method)という単語のほうが一般的だが、HSPの「メソッド」は別の機能(mcall)を指すので避けたい。
インスタンス (instance)
「変数のセット」のこと。直訳すると実体。
メンバ関数の最初の引数に使う。
他の言語では「オブジェクト」(object)とも呼ばれる。
newmod (インスタンスを格納する配列変数), (モジュールクラス名), (コンストラクタ引数...)
新しいインスタンスを作る命令。
受け取る変数は配列として扱われ、空いている要素があれば自動的に再利用する。
delmod (インスタンスを持つ変数)
インスタンスを明示的に壊すための命令。解体した後は“空っぽ”(null)になる。
基本的には使わなくていい。
del は delete (デリート) の略。
varuse( (インスタンスを持つ変数) )
変数がインスタンスを持っているかどうかを調べる関数。
持っていれば真(0 以外の整数値)を、持っていなければ(“空っぽ”(null)なら)偽(0)を返す。
これを使う前に vartype で変数の型を調べておくほうが安全。
foreach (インスタンスを格納する配列)
配列の各要素についてループを行う命令。ただし、インスタンスをもっていない要素についてはスキップする。
repeat と同じように cnt を使うことができる。
モジュール変数型ではない配列でも「repeat length( (配列) )」の代わりにも使える。スキップはされない。

おわりに

さて、今回は「モジュール変数」機能を「使う」側の説明をしました。
次回は、「モジュール変数」機能で「作る」側の話になります。
質問などあれば掲示板までどうぞ。


参考文献

疑いぶかいあなたのためのオブジェクト指向再入門
「マルチプルインスタンス」を中心に捉えてオブジェクト指向プログラミングを解説しているページ。
私のこれはOOPの講座ではありませんが、この改訂版の基本的な着想を与えてくれました。