lua table 解析
什么是table
table 是 Lua 的一种数据结构,用来帮助我们创建不同的数据类型
- 数组
- 字典
- 类
- 队列
- 栈
- …
table键值对 key value
-
key,除了nil,lua的数据类型都可以做为key
t={} t[1] = "int" -- key 可以是整数 t[1.1] = "double" -- key 可以是小数 t[{}] = "table" -- key 可以是表 t[function () end] = "function" -- key 可以是函数 t[true] = "Boolean" -- key 可以是布尔值 t["abc"] = "String" -- key 可以是字符串 t[io.stdout] = "userdata" -- key 可以是userdata t[coroutine.create(function () end)] = "Thread" -- key可以是thread printTable(t)
-
显示结果
{ 1: int 1.1: double function: 0x7fde4640cc50: function abc: String file (0x7fff88ab7028): userdata thread: 0x7fde46405b88: Thread table: 0x7fde4640c0a0: table1 true: Boolean }
-
我们常用的是整数和字符串,也有用到table,若用到其他的多为骚操作
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 struct Table * metatable ; TValue * array ; // array part Node * node ; Node * lastfree ; // any free position is before this position GCObject * gclist ; int sizearray ; // size of ‘array ’ array } Table ; typedef struct Node { TValue i_val; TKey i_key; } Node;
-
字段解析(列几个我们关心的)
- metatable 元表
- flags 是否存在元表
- array 数组
- sizearray 数组大小
- node hash表,每个Node都是一个键值对,node指向哈希表起始地址
- lsizenode 2的幂次方,不是实际大小,即hash表大小的log2
- lastfree 指向node里面最后一个未用的节点
table的创建
-
new 源码
Table *luaH_new (lua_State *L, int narray, int nhash) { Table *t = luaM_new(L, Table); luaC_link(L, obj2gco(t), LUA_TTABLE); t->metatable = NULL; t->flags = cast_byte(~0); /* temporary values (kept only if some malloc fails) */ t->array = NULL; t->sizearray = 0; t->lsizenode = 0; t->node = cast(Node *, dummynode); setarrayvector(L, t, narray); setnodevector(L, t, nhash); return t; }
- 创建是做了一些初始化
- 把新表link到global_state的gc上,并设置标志位
- 初始化表结构,node属性的终止符是一个dummynode,一个全局只读的空哈希表
table的删除
-
free 源码
void luaH_free (lua_State *L, Table *t) { if (t->node != dummynode) luaM_freearray(L, t->node, sizenode(t), Node); luaM_freearray(L, t->array, t->sizearray, TValue); luaM_free(L, t); }
-
如果表有节点项,释放,释放数组项,释放表头结构。
-
比较以下三种方式的区别
-
1、遍历删除
for i, v in pairs(tb) do tb[i] = nil end
-
2、置空删除
tb = {}
-
3、置nil删除
tb = nil
-
-
用代码测试了一下
printGarbage(0) local tb = {} for i = 1, 10000 do tb[i] = {} end collectgarbage("collect") printGarbage(1) for i, v in pairs(tb) do tb[i] = nil end collectgarbage("collect") printGarbage(2) tb = {} collectgarbage("collect") printGarbage(3) tb = nil collectgarbage("collect") printGarbage(4) --输出结果 0 33.9052734375 kb 1 836.8349609375 kb 2 289.9599609375 kb 3 33.9599609375 kb 4 33.9052734375 kb
-
从结果可以看出
- 第一种方式只是把表引用的数据清除掉了,表本身并没有清除,内存还在
- 第二种方式把表引用的数据和表本身都清理掉了,但是会重新申请了一个空表的内存
- 第三种方式则是全部清理掉了
-
所以不要用第一种方式去清空表,内存没有完全回收
-
如果表需要重复利用,用第二种方式会比较好,
-
如果表确定之后都不用了,就用第三种方式
table的插值
-
插值源码
static TValue *newkey (lua_State *L, Table *t, const TValue *key) { Node *mp = mainposition(t, key); if (!ttisnil(gval(mp)) || mp == dummynode) { Node *othern; Node *n = getfreepos(t); /* get a free place */ if (n == NULL) { /* cannot find a free place? */ rehash(L, t, key); /* grow table */ return luaH_set(L, t, key); /* re-insert key into grown table */ } lua_assert(n != dummynode); othern = mainposition(t, key2tval(mp)); if (othern != mp) { /* is colliding node out of its main position? */ /* yes; move colliding node into free position */ while (gnext(othern) != mp) othern = gnext(othern); /* find previous */ gnext(othern) = n; /* redo the chain with `n' in place of `mp' */ *n = *mp; /* copy colliding node into free pos. (mp->next also goes) */ gnext(mp) = NULL; /* now `mp' is free */ setnilvalue(gval(mp)); } else { /* colliding node is in its own main position */ /* new node will go into free position */ gnext(n) = gnext(mp); /* chain new position */ gnext(mp) = n; mp = n; } } gkey(mp)->value = key->value; gkey(mp)->tt = key->tt; luaC_barriert(L, t, key); lua_assert(ttisnil(gval(mp))); return gval(mp); }
-
解析
- 往table中插入新的值,其基本思路是检测key的主位置(main position)是否为空,这里主位置就是key的哈希值在node数组中(哈希表)的位置。
- 若主位置为空,则直接把相应的(key,value)插入 到这个node中。
- 若主位置被占了,检查占领该位置的(key,value)的主位置是不是在这个地方
- 若不在这个地方,则移动占领该位置的 (key,value)到一个新的空node中,并且把要插入的(key,value)插入到相应的主位置;
- 若在这个地方(即占领该位置的 (key,value)的主位置就是要插入的位置),则把要插入的(key,value)插入到一个新的空node中。
- 若找不到空闲位置放置新键值,则进行rehash函数,扩增加或减少哈希表的大小找出新位置,然后再调用luaH_set把要插入的(key,value)到新的哈希表中,直接返回 LuaH_set的结果。
-
rehash
static void rehash (lua_State *L, Table *t, const TValue *ek) { int nasize, na; int nums[MAXBITS+1]; /* nums[i] = number of keys between 2^(i-1) and 2^i */ int i; int totaluse; for (i=0; i<=MAXBITS; i++) nums[i] = 0; /* reset counts */ nasize = numusearray(t, nums); /* count keys in array part */ totaluse = nasize; /* all those keys are integer keys */ totaluse += numusehash(t, nums, &nasize); /* count keys in hash part */ /* count extra key */ nasize += countint(ek, nums); totaluse++; /* compute new size for array part */ na = computesizes(nums, &nasize); /* resize the table to new computed sizes */ resize(L, t, nasize, totaluse - na); }
- rehash首先统计当前table中到底有value值不是nil的键值对的个数,然后根据这个数值确定table中数组部分的大小(其大小保证数组部分的空间利用率必须50%),最后调用luaH_resize函数来重建table。
table的大小
-
#table源码,此函数是用来求知table的长度
int luaH_getn (Table *t) { unsigned int j = t->sizearray; if (j > 0 && ttisnil(&t->array[j - 1])) { /* there is a boundary in the array part: (binary) search for it */ unsigned int i = 0; while (j - i > 1) { unsigned int m = (i+j)/2; if (ttisnil(&t->array[m - 1])) j = m; else i = m; } return i; } /* else must find a boundary in hash part */ else if (isdummy(t->node)) /* hash part is empty? */ return j; /* that is easy... */ else return unbound_search(t, j); } static int unbound_search (Table *t, unsigned int j) { unsigned int i = j; /* i is zero or a present index */ j++; /* find `i' and `j' such that i is present and j is not */ while (!ttisnil(luaH_getint(t, j))) { i = j; j *= 2; if (j > cast(unsigned int, MAX_INT)) { /* overflow? */ /* table was built with bad purposes: resort to linear search */ i = 1; while (!ttisnil(luaH_getint(t, i))) i++; return i - 1; } } /* now do a binary search between them */ while (j - i > 1) { unsigned int m = (i+j)/2; if (ttisnil(luaH_getint(t, m))) j = m; else i = m; } return i; }
-
#table源码分析
- 1、j>0而且数组部分最后一个为空,则进入下面的二分查找,此二分查找就是找到一个i不为空,j为空的的两个索引,当j-i>1就跳出来返回i
- 2、j<=0或者数组最后一个不为空,而且没有hash部分,此时,没办法,没得统计,折中返回数组的长度的。
- 3、j<=0或者数组最后一个不为空,有hash部分,进入到unbound_search,从字面意思,就是在一个乱的区间查找,此函数最终也是一个二分查找,会从数组长度为键递增方式查找是否有值,如果有就继续往后找,没有就返回上一个的下标
-
举例说明
local tb = {} tb.name = "name" tb[1] = 1 tb[2] = 2 tb[3] = 3 tb[4] = 4 tb[5] = 5 -- 因为数组开辟空间是以2的幂次方分配的,这里数组开辟的空间大小是8,即sizearray = 8 #tb = 5 -- 满足第1个判断 tb[8] = 8 #tb = 8 -- 满足第2个判断, tb[9] = 9 -- 由于数组利用率要超过50%,此时9是存储在hash部分,不然开辟16个利用率不够50% #tb = 9 -- 满足第3个判断,进入unbound_search
table的遍历
-
table的遍历分为ipairs和pairs
- ipairs遍历数组部分,ipairs遍历顺序就是从1开始一次加1往后遍历table的数组部分
- pairs遍历整个table,pairs的遍历实际上是调用luaH_next
int luaH_next (lua_State *L, Table *t, StkId key) { int i = findindex(L, t, key); /* find original element */ for (i++; i < t->sizearray; i++) { /* try first array part */ if (!ttisnil(&t->array[i])) { /* a non-nil value? */ setnvalue(key, cast_num(i+1)); setobj2s(L, key+1, &t->array[i]); return 1; } } for (i -= t->sizearray; i < sizenode(t); i++) { /* then hash part */ if (!ttisnil(gval(gnode(t, i)))) { /* a non-nil value? */ setobj2s(L, key, key2tval(gnode(t, i))); setobj2s(L, key+1, gval(gnode(t, i))); return 1; } } return 0; /* no more elements */ }
-
由luaH_next的源代码可以看出,pairs尽管说是随机遍历,但是会有一个原则是先有序遍历数组部分,然后在随机遍历hash部分
-
所以对于数组部分的遍历,ipairs和pairs的结果是没有什么区别的,甚至测试下来pairs的速度更快一丁点
-
测试代码
local tb = {} tb.name = "name" tb[8] = 8 tb[1] = 1 tb[2] = 2 tb[3] = 3 tb[4] = 4 tb[5] = 5 tb[10] = 8 tb.name2 = "name" tb[100] = 8 tb.name1 = "name" tb[20] = 8 for i, v in pairs(tb) do print(v) end --多次执行结果 1 1 2 2 3 3 4 4 5 5 8 8 name2 name 20 8 100 8 10 8 name1 name name name
- 发现数组部分一直最先遍历,而且是有序的,而hash部分的顺序一直在变动
table 传引用
-
table传入函数后,可以改变table的值
local tb = {1,2,3} local dealTbl = function(srcTb) table.insert(srcTb, 4) end dealTbl(tb) printTable(tb) --结果是 1, 2, 3, 4
-
但是这样的方式是没法改变的
local tb = {1,2,3} local dealTbl1 = function(srcTb) srcTb = {} --变量srcTb重新引用到一个新的空表 end dealTbl(tb) printTable(tb) --结果是 1, 2, 3 local tb1 = tb -- tb1引用到{1,2,3} tb = {} --变量tb重新引用到一个新的空表 printTable(tb1) --结果是 1, 2, 3
-
所以当我们在代码里需要改变一个table而需要暂存改table的数据,后续需要还原时,是无需clone的
table 元表
-
setmetatable
local mt = { name = "mt", } local mytbl = { name = "mytbl" } setmetatable(mytbl, mt)
-
getmetatable
getmetatable(mytbl)
-
table 元表-元方法
local a = {1} local b = {2} printTable(a+b)--会报错,a不能运算 local mt = { name = "mt", __add = function(c1, c2) if #c1 == #c2 then local result = {} for i = 1, #c1 do result[i] = c1[i] + c2[i] end return result end return {} end, } local a = {1} local b = {2} setmetatable(a, mt) --正确,可以运算 printTable(a+b)
- 所以在元表里写了__add元方法就可以对table进行+运算了
- 同理这样的方式,可以写很多元方法 - * / 等等都可以
- 元方法
table 元表 __index方法
-
访问 table 的时候,如果这个键没有值,那么Lua就会寻找该table的metatable(假定有metatable)中的__index 键。如果__index包含一个表格,Lua会在表格中查找相应的键
local mt = { name = "mt", __index = function(table, key) print("不存在的key:", key) end, } local mytbl = { name = "mytbl", } setmetatable(mytbl, mt) print(mytbl.name) print(mytbl.grade) --结果是 mytbl 不存在的key: grade nil
- 由此我们可以把__index指向一个表,实现继承
local super = {} super.grade = 1 super.setGrade = function(self, grade) self.grade = grade end super.getGrade = function(self) return self.grade end local mt = { name = "mt", __index = super, } local mytbl = { name = "mytbl", } setmetatable(mytbl, mt) print(mytbl.name) print(mytbl.grade) print(mytbl:getGrade()) --结果是 mytbl 1 1
table 元表__newindex
-
对table不存在的字段进行赋值的时候,想监控这个操作,进行一些额外的处理,这时候就要用到__newindex
local mt = { name = "mt", __newindex = function(table, key) print("不存在的key:", key) end, } local mytbl = { name = "mytbl", } setmetatable(mytbl, mt) mytbl.name = 1 mytbl.grade = 1 --输出 不存在的key: grade
-
__index用于查询,__newindex用于更新
-
__newindex和__index实现readonly
function readOnly(t) local proxy = {} --定义一个空表,访问任何索引都是不存在的,所以会调用__index 和__newindex local mt = { __index = t, ---__index 可以是函数,也可以是table,是table的话,调用直接返回table的索引值 __newindex = function(t,k,v) error("attempt to update a read-only table", 2) end } setmetatable(proxy,mt) return proxy end --test local tb = {"Sunday","Monday","Tuesday","Wednessday","Thursday","Friday","Saturday"} local days = readOnly(tb)
小结
- 之前在没有了解table的时候,可能在代码了写了很多错误或者没必要的方式,后续需要去修改
- 若要充分了解lua table,需要结合源码一起看,并做一些必要的测试