上一篇主要讲了 Lua代码 的运作过程,这一篇主讲 Lua Table 和 基于 MetaTable
实现的 MetaMethod
。
其实我觉得,Lua之所以能大放异彩,其一是它非常精小,其二是其开源,其三则是因为它的MetaMethod
的设计。
Lua 类型
虽然本篇主要讲 table,不过在那之前,最好先来认识一下 Lua 其他类型在 Lua解释器中的实现。
UserData 暂且不谈,NUMBER细分为浮点数和整数,字符串则分长短字符串,函数又分Lua函数和C函数还有轻量的C函数,这一部分会分别留到字符串和闭包的时候再谈论。
1 |
Table
先来想想,我们一般是怎么使用 table 的,是不是大部分时候都是既用来当数组又用来当哈希表。
因此,可以很简单的想到,table 很有可能底层是使用哈希表来实现的。事实上Lua早期版本也确实是这么做的,只不过后来优化了 table 被当做数组用的性能(就是加了个数组)。
可以看到 Table 的结构中,有表示 metatable,也有数组,还有哈希表,跟我们猜想的几乎一致。而且这还更激进一点,两者都启用!
注意到 lsizenode
是以2位低的整数次幂,非实际大小。
1 | typedef struct Table { |
数组部分没什么好看的,我们主要看其哈希表的实现。 TKey
中的 nk
主要是用来当Key的哈希值相同时,开链用。
1 | typedef union TKey { |
创建 table
创建 table 主要是对结构进行初始化,同时注意到一点,table 的 node 默认是 dummynode
,在lua设计中,当一个table的哈希表部分为空时,则默认使用一个 dummynode
的全局对象,因为是只读访问,没有线程安全问题,其实设置成 NULL
我想也是可以的,不过还记得上面的 lsizenode
是以2为底的幂次吗?2^0 == 1,因此设置一个 dummynode
,逻辑看起来更自然。不过如果你不小心链接了两次 Lua 库,内存上就有两份 dummynode
,根据 dummynode
运算的逻辑都将是 未定义行为。
1 |
|
数组还是哈希表?
经过以上,我们可能会思考,我对这个table的操作,到底是操作了数组还是哈希表?在这里我们来看看以下几个操作。
1 | local a = {1, 2, 3} |
可以看出,第一行的操作指令是 SETLIST
,而第二行则是 SETTABLE
。
SETLIST
SETLIST 这种操作默认是在 数组中的,因此会先检查 table 中数组的大小,然后进行赋值。 luaH_setint
会调用 luaH_newkey
通过哈希获取 Key 应当存在的位置,然后将其放入。
1 | vmcase(OP_SETLIST) { |
luaH_resize
会对数组和哈希表进行扩容or缩容,数组中 nil的值 将会被省略。
SETTABLE
这个操作就得根据情况来判断了,但最终都是调用到了 luaH_newkey
这个函数。如果不是个 table,则检查其元方法是否存在,检查方法就是根据 table 结构中的 flags
字段按位来找是否有元方法。查找元方法的路径不能过长,默认是 MAXTAGLOOP
2000。
1 |
|
luaH_newkey
根据 哈希规则,找到 mp即在哈希表中应该存放key的位置,如果被用掉了,就检查占据这个位置的键的位置是不是真的就在这(通过哈希,你可以理解为线性探查法),若真在这,就通过左移 lastfree
指针,找一个新位置,然后将其链起来。否则的话,老让给新的,老重新哈希找到合适的位置,如果还冲突继续往左走。(我个人觉得像是 线性探查+开链法的结合体)
1 | TValue *luaH_newkey (lua_State *L, Table *t, const TValue *key) { |
如果 getfreepos
找不到合适的位置(lastfree 走到最左边),则 调用 rehash
。
里面会统计数组大小,哈希表中可以合入数组的大小(就是看一下key是不是能转换成整数)。
1 | static void rehash (lua_State *L, Table *t, const TValue *ek) { |
Table 长度怎么算?
Lua 中取长度采用 #
号获取,它会调用以下函数。
如果存在数组部分,则采用二分查找找到第一个 t[i] ≠nil && t[i + 1] = nil
,如果数组真的全在里面,才会走到哈希表的计算。isdummy 为 ((t)->lastfree == NULL)
,如果哈希表部分为空,就不算哈希部分呗,如果有,就在哈希表里面二分查找,将整数下标中的个数给加入进来。因此永远不要对非序列进行取长度操作。
1 | static lua_Unsigned unbound_search (Table *t, lua_Unsigned j) { |
MetaMethod
前面提到过,table 的结构有个 flags
字段,表示哪些元方法不存在!然后对 一个类型操作时,会去检查其元方法,如果有元方法,则尝试调用,最多调用2000次,超过则抛出错误。同时会对 元方法的名字,进行优化,提前创建好这些字符串对象,并将其缓存起来。
1 | void luaT_init (lua_State *L) { |
pairs与ipairs
table 最常用的两种遍历操作,pairs 是通过 luaH_next
函数实现的。当key 为nil时,则从头开始遍历。
1 | int luaH_next (lua_State *L, Table *t, StkId key) { |
需要注意的是,如果 table 中某个键的值被设置为nil,有可能会被GC回收,但是此时还在遍历,Lua官方称其为死键。
其实也没做什么特殊的,标志为死键又不是被删除了,不过如果被 rehash
则会被从哈希表清除,触发 rehash
的条件是添加新键且空间不够了,因此如果你不添加新键,遍历就挺安全的。
1 | static unsigned int findindex (lua_State *L, Table *t, StkId key) { |
至于 ipairs 则是通过 lua_geti
实现,其真正的操作是在 luaH_getint
中,如果还是找不到,则会通过 luaV_finishget
去找其元方法。ipairs 当遍历到 nil
时则会停止,要特别注意不能有黑洞。
1 | LUA_API int lua_geti (lua_State *L, int idx, lua_Integer n) { |