Lua Table的设计与实现
Lua 版本 5.3.4
最好能够使用gdb
1.数据结构
1.1 key
typedef union TKey {
struct {
TValuefields;
int next; /* for chaining (offset for next node) */
/* 这个是offset, 不是指针*/
} nk;
TValue tvk;
} TKey;
1.2 hash 节点 Node
typedef struct Node {
TValue i_val;
TKey i_key;
} Node;
1.3 Table
typedef struct Table {
CommonHeader;
lu_byte flags; /* 1<<p means tagmethod(p) is not present */
lu_byte lsizenode; /* log2 of size of 'node' array */
unsigned int sizearray; /* size of 'array' array */
TValue *array; /* array part */
Node *node;
Node *lastfree; /* any free position is before this position */
struct Table *metatable;
GCObject *gclist;
} Table;
<1> 从上面结构和注释可以看出,Lua的table 分为 数组和哈希两个部分
<2> lsizenode 不是哈希节点的大小,而是log2 of 哈希大小,哈希部分一定是2的整数次幂
2. 初始化
2.1 初始化函数
Table *luaH_new (lua_State *L) {
GCObject *o = luaC_newobj(L, LUA_TTABLE, sizeof(Table));
Table *t = gco2t(o);
t->metatable = NULL;
t->flags = cast_byte(~0);
t->array = NULL;
t->sizearray = 0;
setnodevector(L, t, 0);
return t;
}
2.2 初始化说明
<1> hash 部分的node, lastfree, lsizenode
是在 setnodevector
初始化,当是空table的时候,node 初始化为所有table 哈希部分为空共享的一个静态变量 dummynode
3. table 插入
无论是数组部分还是哈希部分,赋值的入口都是在 lvm.c: void luaV_execute 中的:
vmcase(OP_SETTABLE) {
TValue *rb = RKB(i);
TValue *rc = RKC(i);
settableProtected(L, ra, rb, rc);
vmbreak;
}
从其中的 宏定义 settableProtected(L, ra, rb, rc)
展开可以看出:
(假设t为table, 键为 k, 值为v)
如果 t[k] 不为nil, 直接赋值; 如果t[k]为nil, 需要添加新的键,然后向对应的值赋值
其中通过 luaH_get 函数知道t[k]是否为空,通过 luaH_newkey 函数添加新的键
3.1 luaH_get 函数
3.1.1 函数代码
const TValue *luaH_get (Table *t, const TValue *key) {
switch (ttype(key)) {
case LUA_TSHRSTR: return luaH_getshortstr(t, tsvalue(key));
case LUA_TNUMINT: return luaH_getint(t, ivalue(key));
case LUA_TNIL: return luaO_nilobject;
case LUA_TNUMFLT: {
lua_Integer k;
if (luaV_tointeger(key, &k, 0)) /* index is int? */
return luaH_getint(t, k); /* use specialized version */
/* else... */
} /* FALLTHROUGH */
default:
return getgeneric(t, key);
}
}
我们常用的key类型是 string和int, 着重看这两个部分。
3.1.2 获取key为字符串的值
先根据字符串的hash值,获取其在table中的主节点位置,然后通过主节点的next,逐个比较节点的key 和查找的key,如果相同,返回。
3.1.3 获取key为整数的值
(假设t为table, 键为 key, 值为val)
如果 1<= key && key <= t->sizearray
,说明是在数组部分,直接返回数组部分内容即可 return t->array[key-1]
;
否则 通过key计算哈希值-> 通过哈希值计算在 node 数组中的主位置节点 mainpos -> 如果 mainpos 非空且mainpos已有的 键k和查找的key相等,则返回;否则查找mainpos 的下一个节点,如果不为空且键和key相等,那么就返回该节点。。。 如此循环,直到找到相同的键或者该哈希值所在链的末尾。
3.1.4 实例
(待补充)
3.2 luaH_newkey 函数
3.2.1 函数代码
TValue *luaH_newkey (lua_State *L, Table *t, const TValue *key) {
Node *mp;
TValue aux;
if (ttisnil(key)) luaG_runerror(L, "table index is nil");
else if (ttisfloat(key)) {
lua_Integer k;
if (luaV_tointeger(key, &k, 0)) { /* does index fit in an integer? */
setivalue(&aux, k);
key = &aux; /* insert it as an integer */
}
else if (luai_numisnan(fltvalue(key)))
luaG_runerror(L, "table index is NaN");
}
mp = mainposition(t, key);
if (!ttisnil(gval(mp)) || isdummy(t)) { /* main position is taken? */
Node *othern;
Node *f = getfreepos(t); /* get a free place */
if (f == NULL) { /* cannot find a free place? */
rehash(L, t, key); /* grow table */
/* whatever called 'newkey' takes care of TM cache */
return luaH_set(L, t, key); /* insert key into grown table */
}
lua_assert(!isdummy(t));
othern = mainposition(t, gkey(mp));
if (othern != mp) { /* is colliding node out of its main position? */
/* yes; move colliding node into free position */
while (othern + gnext(othern) != mp) /* find previous */
othern += gnext(othern);
gnext(othern) = cast_int(f - othern); /* rechain to point to 'f' */
*f = *mp; /* copy colliding node into free pos. (mp->next also goes) */
if (gnext(mp) != 0) {
gnext(f) += cast_int(mp - f); /* correct 'next' */
gnext(mp) = 0; /* now 'mp' is free */
}
setnilvalue(gval(mp));
}
else { /* colliding node is in its own main position */
/* new node will go into free position */
if (gnext(mp) != 0)
gnext(f) = cast_int((mp + gnext(mp)) - f); /* chain new position */
else lua_assert(gnext(f) == 0);
gnext(mp) = cast_int(f - mp);
mp = f;
}
}
setnodekey(L, &mp->i_key, key);
luaC_barrierback(L, t, key);
lua_assert(ttisnil(gval(mp)));
return gval(mp);
}
<1> 函数的主流程:开始部分是处理newkey为 浮点数的情况。
mp = mainposition(t, key) :根据当前key值,获取在table中的主位置。和上面 luaH_get 中的获取方式一样。
1. 如果mp所在位置没有被占用,返回mp节点的value值
2. 如果当前位置被占用
2.1 寻找一个 空闲 node, 名称f (getfreepos函数, 利用Table 结构中的 lastfree 成员)
如果f 为空,那么需要调用 rehash,重新调整(扩容)table,然后调用 luaH_set 赋值,return
(rehash 是table 管理空间大小的核心函数,介绍见下面)
2.2 寻找mp节点的key 的主节点位置,命名为 othern
2.2.1 如果 othern != mp ,说明 当前mp节点位置的key 占用了 newkey 的主节点位置,那么下面需要将mp节点的键值对移动到其他位置,然后用newkey 占用,具体过程如下:
a. 根据othern ,逐个next,获得 mp的上一个节点(到2.2.1 分支,说明othern 和mp的哈希值是一样的,链表的头结点是othern)
b. 将mp 值复制到 前面找到的空闲节点 f
c. 如果mp还有下个节点,需要将f 的下个节点修正,将mp的next 置为0.最后将mp的值置为nil(此时mp位置就可以放置新的键值对了)
2.2.2 如果 ohtern == mp, 说明mp节点的key和newkey 主位置一致,要串在统一链表上。后续操作就是插入这个链表。