その4 会得必須!Luaの真髄「テーブル」
C言語との親和性、複数の戻り値のサポート、コルーチンと魅力的な機能を沢山持つLuaですが、実はLuaの真髄は「テーブル」にあります。
テーブルというのは、イメージするなら以下のような名前がついた変数の塊です:
各変数それぞれではなくてこの塊が一つのテーブルです。テーブルはLuaが扱うことができる変数型の一つで、保持している値一つ一つに必ず名前が付けられます。値はLuaが定義する型ならば何でも入れられます。Lua側でテーブルを作るには次のようにします:
tbl = {
Name = "Hoge",
Age = 20,
Hometown = "北海道"
};
C言語の構造体に似ていますが、あくまでもこういうデータの塊を表しています。tbl型というわけではないので注意です。値へアクセスするには以下のようにテーブル変数の名前の後にコンマを付けてデータ名を指定します:
myAge = tbl.Age;
myTown = tbl.Hometown;
値を変更するのも同様です:
tbl.Age = 30;
tbl.Hometown = "東京都";
ここまでは、「ん〜、まぁ、データの塊なのね」という感じだと思うのですが、テーブルにはLuaが定義する型ならば何でも入れられるという所に最大のポイントがあります。Luaの変数型には数値や文字列はもちろんですが、関数やテーブルもあります。関数を入れるとはどういう事なのか?次で説明します。
@ テーブルに関数を入れる
Luaでは関数も値の一つとして考えられているため、次のように変数に代入することができます:
function Func(str)
print(str);
end
MyFunc = Func;
C言語で言う関数ポインタのような感覚です。上のように代入したら、Func関数の代わりにMyFunc関数として同じ関数を呼び出す事ができるようになります:
MyFunc("Marupeke");
MyFuncの宣言と同時に関数を代入するために、次のような書き方ができます:
MyFunc = function(str)
print(str);
end
functionの後に名前を付けない、いわゆる「無名関数」にするのがポイントです。
さて、上のような「変数名=値」という部分は、テーブル宣言でそっくりそのまま同じように書けるわけです:
tbl = {
MyFunc = function(str)
print(str);
end
};
こうすると、下のようにテーブルに登録した関数を呼び出せます:
tbl.MyFunc("Marupeke");
この書き方、「C++にクラスのメソッド呼び出し」に非常に似てますよね。そう、テーブルを使うとLuaでクラスのような書き方ができるようになるんです。もちろん、C++にある言語仕様のクラスほど頑健では無いのですが、似た様な事ができるんです。ここに、「C++とLuaの連携」という道が見えてきます。
A クラス宣言+オブジェクト生成をするNew関数
テーブルに関数を登録するとクラスのメソッド呼び出しみたいな事ができるようになります。ただ、クラスにはもう一つ大切な「メンバ変数」があります。例えば下のようなクラス(構造体)をC言語側で作ったとして、
C言語でのクラス(構造体) struct Data {
string Name;
int Age;
string Hometown;
};
Data obj1, obj2;
インスタンスを2つ作れば2つ分の個別データになりますよね。Lua側のテーブルでも同じことをしようと思ったら、テーブルをその度に作る必要があるわけです。ですから、「テーブルを作る関数」をLua側に設定する必要があります。
どう作っても良いのですが、どうせならば「クラス名.New()」としたらテーブルが吐き出されるようにしたい所です。この書き方をLuaでするにはクラス名のテーブルを作ればいいんですよね。そしてNew関数内はこうなります:
オブジェクト生成 Data = {
New = function()
return {
Name = "def",
Age = 0,
Hometown = "def"
};
end
};
obj1 = Data.New();
obj2 = Data.New();
Dataという構造体っぽいテーブルを作ります。中にはNewという名前の関数を保持しています。New関数はテーブルを返しています。Newを呼ぶ度に新しいテーブルが作成されますので、これはインスタンス作成関数になっているわけです。
さて、クラスには「メソッド」も必要です。C++のクラスのメソッドは「自分が持っているメンバを使う」という特色があります。例えばデータを表示するprintメソッドをDataテーブルに追加してみます:
printメソッド追加…? Data = {
New = function()
return {
Name = "def",
Age = 0,
Hometown = "def"
print = function()
print(Name, Age, Hometown);
end
};
end
};
obj1 = Data.New();
obj1.print();
print関数を新しく定義して、テーブル内の変数を使っているように見えるのですが、これは残念ですがうまく動きません。エラーにはならないのですが、print関数内で「Name, Age, Hometown」という3つの変数が空宣言されたとみなされるので、出力結果は全部「nil」になってしまいます。
C++のクラスは、自分の持っているメンバにアクセスするためにメソッド呼び出し時に暗黙的に「thisポインタ」を渡しています。このthisに相当するものが必要なんです。
そこで、print関数の引数にthisを渡してしまう事にします:
printメソッド追加! Data = {
New = function()
return {
Name = "def",
Age = 0,
Hometown = "def"
print = function(this)
print(this.Name, this.Age, this.Hometown);
end
};
end
};
obj1 = Data.New();
obj1.print(obj1);
一番下のコードでobj1テーブルが持つprint関数に自分自身を渡しています。自分の持っている値を自分の持っている関数に処理してもらっているわけで、これは嬉しいことに成功します。obj1テーブルにあるName, Age, Hometownが正しく出力されます。C++の場合も実はメソッドには暗黙的にthisが渡されているのですが、それをLua側で明示的に渡してあげればいいんです。明快。
ただ…呼び出す時に毎回自分の変数名を入れるのはちょっと冗長な感じもします。そのため、Luaでは「テーブルに登録した関数を呼び出すときに『:』を使うと、テーブル自体を第1引数に渡す」というシンタックスシュガーがあるんです。どういう事かというと次のような呼び出しができるんです:
呼び出しのシンタックスシュガー Data = {
New = function()
return {
Name = "def",
Age = 0,
Hometown = "def"
print = function(this)
print(this.Name, this.Age, this.Hometown);
end
};
end
};
obj1 = Data.New();
obj1:print();
先との違いがわかりますでしょうか?print関数の呼び出しにコンマ(.)の替りにコロン(:)を使っています。これでprint関数の第1引数に暗黙的にobj1自身が渡されるんです。これは正にC++のメソッドを呼び出した時と同じ挙動です。これでメソッドも定義できました。
Lua側でクラスを宣言するかのようなNew関数を作れば、わりと簡単にクラスっぽい事ができるわけです。
B クラスっぽい部分だけを外部ファイルに置いて呼び出す
クラスっぽいことはできるようになったわけですが、毎回これをLuaファイルに書くのはもちろんシンドイわけです。外部ファイルにこの宣言部分だけを記述して、別のLuaファイルではそれを呼び出して使えるようにすると便利です。C言語でいう「#include」が欲しいというわけです。
Luaには「require」というC言語の#includeに相当するライブラリ関数があります。先程のクラス宣言部分をClassData.luaファイルに保存したとして、使う側では次のようにrequire関数でそれを宣言します:
require関数を使う require("ClassData");
obj1 = Data.New();
obj1:print();
ファイル名を指定するのですが「拡張子をつけない」事に注意して下さい。付けると別挙動になってしまいます。これでこの関数が呼び出された時にClassData.luaがLuaステートにより実行・登録されます。これでコードの再利用がどんどんできるようになるわけです。ただし、同じLuaステートで同じファイルがrequireされる度にコードが走ります。これは大量のLuaファイルを扱うときに死活問題となります。C++の場合は、
C言語での重複防止 #ifndef IKD_OX_DATA_H
#define IKD_OX_DATA_H
...
#endif
と重複インクルードを避けています。同じようにLuaの独自ライブラリファイルを作った時にも、
requireの重複防止(ClassData.lua) if (IKD_OX_LUA_DATA_H == nil) {
IKD_OX_LUA_DATA_H = 1
Data = {
...
};
}
という対処が必要になるかもしれません。
ClassData.luaを別フォルダに入れている時は、そこまでの相対パスを使って指定することもできます:
require関数を使う(相対パス) require("code/ClassData")
obj1 = Data.New();
obj1:print();
Luaのテーブル機能はこういう幅広い機能拡張を作ることができ、大変面白い仕組みだと思います。さて、ではこれとC++側をどうリンクさせていけば良いのか。次はその辺りを考えてみます。