ltable
テーブルは Lua の最も重要なデータ構造だ。というか、コレクションタイプのデータ構造をこれしか持たない(配列とかは Lua にはない)。さて、ヘッダファイルを見ると、 luaH_ なるプレフィクス(Hはハッシュ表の H だろう)を持つ関数たちが用意されている。
まずはデータ構造からおさらいする。 Table は lobject.h に定義されている型で、テーブルを表す。
/* ** Tables */ typedef struct Node { TObject i_key; TObject i_val; struct Node *next; /* for chaining */ } Node; typedef struct Table { CommonHeader; lu_byte flags; /* 1<<p means tagmethod(p) is not present */ lu_byte lsizenode; /* log2 of size of `node' array */ struct Table *metatable; TObject *array; /* array part */ Node *node; Node *firstfree; /* this position is free; all positions after it are full */ GCObject *gclist; int sizearray; /* size of `array' array */ } Table;
けっこう複雑だが、まずもって目につくのは array パートというのがあるという事実だ。なーんだ、そういう最適化はしているのね、と今更ながらに思う。残りのパートはよくわからないが、 node がノードリストであろうという気がする。なにせキーとバリューの構造体だ。でもなぜ linked list なのだろう。ハッシュの衝突かねぇ。
テーブルを作成する luaH_new はこうなっている。
Table *luaH_new (lua_State *L, int narray, int lnhash) { Table *t = luaM_new(L, Table); luaC_link(L, valtogco(t), LUA_TTABLE); t->metatable = hvalue(defaultmeta(L)); t->flags = cast(lu_byte, ~0); /* temporary values (kept only if some malloc fails) */ t->array = NULL; t->sizearray = 0; t->lsizenode = 0; t->node = NULL; setarrayvector(L, t, narray); setnodevector(L, t, lnhash); return t; }
luaM_new は luaM_malloc をキャストつきで呼び出すマクロ。 luaC_link はガベコレまわりのようなので後で見ることにするが、たぶんマーク&スイープするときに「Lから手繰っていくリンク」を登録するのではないかと思う。たしか Lua は世代的な GC だったはずだ。
メタテーブルは、たしか演算子の意味等を変えるためのものだ。最初はデフォルト値を渡す設計なのだろう(そしてデフォルト値は lua_State ごとに切り替えられると)。
最後の setarrayvector と setnodevector は、メモリを確保して nil をセットしているだけなので省略。というわけで luaH_new は、必要そうなメモリを確保するところまでをやるということだ。実際にいくつかの値をもとにテーブルを構成する場合は、まず必要な数のメモリを確保してから、書き込み作業を行うというわけだろう。
つぎにテーブルへのアクセス。まずは get の方が(簡単そうなので)見てみる。luaH_get は、見てみるとキーの型からディスパッチしているだけの関数で、実際には luaH_getstr (文字列)、luaH_getnum (数値)、 luaH_getany (それ以外)の3つに分けられる。順番に見るのだが、ハッシュ値を計算するところはマクロを(原意を損ねない程度に)展開しておく。
const TObject *luaH_getstr (Table *t, TString *key) { Node *n = t->node[lmod(key->tsv.hash, t->lsizenode)]; do { /* check whether `key' is somewhere in the chain */ if (ttisstring(n->i_key) && tsvalue(n->ikey) == key) return n->i_val; /* that's it */ else n = n->next; } while (n); return &luaO_nilobject; }
キーが文字列の場合、すでにハッシュ値は計算されているからそれを使う。発見されたノードは基本的に同じハッシュ値のはずなので、やはり同じ場合はリストで連結しているようだ。発見されない場合は nilobject が返る。ところで、内容が同じ文字列のとき、 luaS_new を使っていると常に同じデータオブジェクトを参照するはずなので、ポインタの一致で同等性を判断している。
const TObject *luaH_getnum (Table *t, int key) { if (1 <= key && key <= t->sizearray) return &t->array[key-1]; else { lua_Number nk = cast(lua_Number, key); Node *n = hashnum(t, nk); do { /* check whether `key' is somewhere in the chain */ if (ttisnumber(n->i_key) && nvalue(n->i_key) == nk) return gval(n); /* that's it */ else n = n->next; } while (n); return &luaO_nilobject; } }
数値がキーのときは、まず配列パートを探索してみるというステップが入るようだ。 hashnum はマクロではなくちゃんとした関数で、計算はちょっと面倒くさいのだが、端的に言うと lua_Number が int より長い場合を考慮にいれていろいろやっている。手元の環境では lua_Number は double になっている。
static const TObject *luaH_getany (Table *t, const TObject *key) { if (ttisnil(key)) return &luaO_nilobject; else { Node *n = luaH_mainposition(t, key); do { /* check whether `key' is somewhere in the chain */ if (luaO_rawequalObj(gkey(n), key)) return gval(n); /* that's it */ else n = n->next; } while (n); return &luaO_nilobject; } }
getany の場合、ハッシュ値は mainposition という関数で計算されている。これはやっぱり型でディスパッチする関数であるが、だいたいポインタの値(アドレス)をもとにハッシュ値を計算すると考えてよいようだ。