常用 table 方法
table 库是由一些辅助函数构成的,这些函数将 table 作为数组来操作。
下标从 1 开始
在 Lua 中,数组下标从 1 开始计数。
官方解释:Lua lists have a base index of 1 because it was thought to be most friendly for non-programmers, as it makes indices correspond to ordinal element positions.
确实,对于我们数数来说,总是从 1 开始数的,而从 0 开始对于描述偏移量这样的东西有利。而 Lua 最初设计是一种类似 XML 的数据描述语言,所以索引(index)反应的是数据在里面的位置,而不是偏移量。
在初始化一个数组的时候,若不显式地用 键值对 方式赋值,则会默认用数字作为下标,从 1 开始。由于在 Lua 内部实际采用哈希表和数组分别保存键值对、普通值,所以不推荐混合使用这两种赋值方式。
local color={first="red", "blue", third="green", "yellow"}
print(color["first"]) --> output: red
print(color[1]) --> output: blue
print(color["third"]) --> output: green
print(color[2]) --> output: yellow
print(color[3]) --> output: nil
从其他语言过来的开发者会觉得比较坑的一点是,当我们把 table 当作栈或者队列使用的时候,容易犯错:
- 追加到 table 的末尾用的是
s[#s+1] = something
,而不是s[#s] = something
,而且如果这个 something 是一个 nil 的话,会导致这一次压栈(或者入队列)没有存入任何东西,#s
的值没有变。 示例如下:-- 情况 1: 使用 `#str+1`, 要添加的值为“真值”或 false str = {'a', 'b', 'c'} str[#str+1] = 'd' print('str_len:' .. #str) -- 打印出 str 的元素个数 for i,v in ipairs(str) do print(i,v) -- 打印 str 的元素 end -- output: (正确的结果) str_len:4 1 a 2 b 3 c 4 d -- 注:false 也是可以正常添加的。 -- 情况 2: 使用 `#str+1`, 要添加的值为 nil str = {'a', 'b', 'c'} str[#str+1] = nil print('str_len:' .. #str) -- 打印出 str 的元素个数 for i,v in ipairs(str) do print(i,v) -- 打印 str 的元素 end -- output: (正确的结果) str_len:3 1 a 2 b 3 c -- str 的元素个数没有变化 -- 情况 3: 使用 `#str`, 要添加的值为“真值”或 false str = {'a', 'b', 'c'} str[#str] = 'd' print('str_len:' .. #str) -- 打印出 str 的元素个数 for i,v in ipairs(str) do print(i,v) -- 打印 str 的元素 end -- output: (不期望的结果) str_len:3 1 a 2 b 3 d -- str 的元素个数没有变化,但是最后一个元素被覆盖了,并不是期望的添加一个新元素 -- 情况 4: 使用 `#str`, 要添加的值为 nil str = {'a', 'b', 'c'} str[#str] = nil print('str_len:' .. #str) -- 打印出 str 的元素个数 for i,v in ipairs(str) do print(i,v) -- 打印 str 的元素 end -- output: (不期望的结果) str_len:2 1 a 2 b -- str 的元素少了一个,不光没有添加反而删除了一个
- 如果
s = { 1, 2, 3, 4, 5, 6 }
,你令s[4] = nil
,#s
会令你“匪夷所思”地变成 3。 示例如下:s = { 1, 2, 3, 4, 5, 6 } s[4] = nil print('s_len:' .. #s) -- 打印出 s 的元素个数 for i,v in ipairs(s) do print(i,v) -- 打印 s 的元素 end -- output: s_len:3 1 1 2 2 3 3
table.getn 获取长度
取长度操作符写作一元操作 #
。字符串的长度是它的字节数(就是以一个字符一个字节计算的字符串长度)。
对于常规的数组,里面从 1 到 n 放着一些非空的值的时候,它的长度就精确的为 n,即最后一个值的下标。如果数组有一个“空洞”(就是说,nil 值被夹在非空值之间),那么 #t
可能是指向任何一个是 nil 值的前一个位置的下标(就是说,任何一个 nil 值都有可能被当成数组的结束)。这也就说明对于有“空洞”的情况,table 的长度存在一定的 不可确定性。
local tblTest1 = { 1, a = 2, 3 }
print("Test1 " .. table.getn(tblTest1))
local tblTest2 = { 1, nil }
print("Test2 " .. table.getn(tblTest2))
local tblTest3 = { 1, nil, 2 }
print("Test3 " .. table.getn(tblTest3))
local tblTest4 = { 1, nil, 2, nil }
print("Test4 " .. table.getn(tblTest4))
local tblTest5 = { 1, nil, 2, nil, 3, nil }
print("Test5 " .. table.getn(tblTest5))
local tblTest6 = { 1, nil, 2, nil, 3, nil, 4, nil }
print("Test6 " .. table.getn(tblTest6))
我们使用 Lua 5.1 和 LuaJIT 2.1 分别执行这个用例,结果如下:
# lua test.lua
Test1 2
Test2 1
Test3 3
Test4 1
Test5 3
Test6 1
# luajit test.lua
Test1 2
Test2 1
Test3 1
Test4 1
Test5 1
Test6 1
这一段的输出结果,就是这么 匪夷所思。请问,你以后还敢在 Lua 的 table 中用 nil 值吗?如果你继续往后面加 nil,你可能会发现点什么。你可能认为你发现的是个规律。但是,你千万不要认为这是个规律,因为这是错误的。
不要在 Lua 的 table 中使用 nil 值,如果一个元素要删除,直接 remove,不要用 nil 去代替。
table.concat (table [, sep [, i [, j ] ] ])
对于元素是 string 或者 number 类型的表 table,返回 table[i]..sep..table[i+1] ··· sep..table[j]
连接成的字符串。填充字符串 sep 默认为空白字符串。起始索引位置 i 默认为 1,结束索引位置 j 默认是 table 的长度。如果 i 大于 j,返回一个空字符串。
示例代码
local a = {1, 3, 5, "hello" }
print(table.concat(a)) -- output: 135hello
print(table.concat(a, "|")) -- output: 1|3|5|hello
print(table.concat(a, " ", 4, 2)) -- output:
print(table.concat(a, " ", 2, 4)) -- output: 3 5 hello
当需要循环拼接字符时,推荐使用table.concat
。因为若使用..
在循环中拼接字符,会产生大量的中间字符串。如果拼接的字符串很大,经过多次循环拼接后,其内存开销急剧增大,也会同时触发多次GC
,甚至会导致 Lua 虚拟机内存不足。
问题示例代码
local chunk, eof = ngx.arg[1], ngx.arg[2]
if not ngx.ctx.buffer then
ngx.ctx.buffer = ""
end
if eof then
local body = body_filter.transform_json_body(match_t, ngx.ctx.buffer) --calc
ngx.arg[1] = body
else
ngx.ctx.buffer = ngx.ctx.buffer .. chunk
ngx.arg[1] = nil
end
问题现象:当响应 body 较大时,luajit 虚拟机会概率性报错内存不足
not enough memory
,同时 openresty 占用的内存居高不下。
正确代码
local chunk, eof = ngx.arg[1], ngx.arg[2]
if not ngx.ctx.buffer_table then
ngx.ctx.buffer_table = {}
end
if eof then
local buffer = table.concat(ngx.ctx.buffer_table) -- 使用 table.concat 拼接
local body = body_filter.transform_json_body(match_t, buffer)
ngx.arg[1] = body
else
ngx.arg[1] = nil
table.insert(ngx.ctx.buffer_table, chunk)
end
table.insert (table, [pos ,] value)
在(数组型)表 table 的 pos 索引位置插入 value,其它元素向后移动到空的地方。pos 的默认值是表的长度加一,即默认是插在表的最后。
示例代码
local a = {1, 8} --a[1] = 1,a[2] = 8
table.insert(a, 1, 3) --在表索引为 1 处插入 3
print(a[1], a[2], a[3])
table.insert(a, 10) --在表的最后插入 10
print(a[1], a[2], a[3], a[4])
-->output
3 1 8
3 1 8 10
table.maxn (table)
返回(数组型)表 table 的最大索引编号;如果此表没有正的索引编号,返回 0。
当长度省略时,此函数通常需要 O(n)
的时间复杂度来计算 table 的末尾。因此用这个函数省略索引位置的调用形式来作 table 元素的末尾追加,是高代价操作。
示例代码
local a = {}
a[-1] = 10
print(table.maxn(a))
a[5] = 10
print(table.maxn(a))
-->output
0
5
此函数的行为不同于 #
运算符,因为 #
可以返回数组中任意一个 nil 空洞或最后一个 nil 之前的元素索引。当然,该函数的开销相比 #
运算符也会更大一些。
table.remove (table [, pos])
在表 table 中删除索引为 pos(pos 只能是 number 型)的元素,并返回这个被删除的元素,它后面所有元素的索引值都会减一。pos 的默认值是表的长度,即默认是删除表的最后一个元素。
示例代码
local a = { 1, 2, 3, 4}
print(table.remove(a, 1)) --删除索引为 1 的元素
print(a[1], a[2], a[3], a[4])
print(table.remove(a)) --删除最后一个元素
print(a[1], a[2], a[3], a[4])
-->output
1
2 3 4 nil
4
2 3 nil nil
table.sort (table [, comp])
按照给定的比较函数 comp 给表 table 排序,也就是从 table[1] 到 table[n],这里 n 表示 table 的长度。 比较函数有两个参数,如果希望第一个参数排在第二个的前面,就应该返回 true,否则返回 false。 如果比较函数 comp 没有给出,默认从小到大排序。
示例代码
local function compare(x, y) --从大到小排序
return x > y --如果第一个参数大于第二个就返回 true,否则返回 false
end
local a = { 1, 7, 3, 4, 25}
table.sort(a) --默认从小到大排序
print(a[1], a[2], a[3], a[4], a[5])
table.sort(a, compare) --使用比较函数进行排序
print(a[1], a[2], a[3], a[4], a[5])
-->output
1 3 4 7 25
25 7 4 3 1
table 其他非常有用的函数
LuaJIT 2.1 新增加的 table.new
和 table.clear
函数是非常有用的。前者主要用来预分配 Lua table 空间,后者主要用来高效的释放 table 空间,并且它们都是可以被 JIT 编译的。具体可以参考一下 OpenResty 捆绑的 lua-resty-* 库,里面有些实例可以作为参考。
判断数组大小
table.getn(t)
等价于 #t
但计算的是数组元素,不包括 hash 键值。而且数组是以第一个 nil
元素作为结束标志的。#
只计算 array 的元素个数,它实际上调用了对象的 metatable
的 __len
函数。对于有 __len
方法的函数返回函数返回值,不然就返回数组成员数目。
Lua 中,数组的实现方式类似于 C++ 中的 map,数组中的所有值,都是以 键值对 的形式来存储(无论是显式还是隐式)。
Lua 内部实际采用 哈希表 和 数组 分别保存 键值对、普通值,所以不推荐混合使用这两种赋值方式。
尤其需要注意的一点是:Lua 数组中允许 nil
值的存在,但是数组默认结束标志却是 nil
。这类比于 C 语言中的字符串,字符串中允许 ‘\0’ 存在,但当读到 ‘\0’ 时,就认为字符串已经结束了。
初始化是例外,在 Lua 相关源码中,初始化数组时:
- 首先判断数组的长度,若长度大于 0 ,并且最后一个值不为
nil
,返回包括nil
的长度; - 若最后一个值为
nil
,则返回截至第一个非nil
值的长度。
注意:一定不要使用 #
操作符或 table.getn
来计算包含 nil
的数组长度,这是一个未定义的操作,不一定报错,但不能保证结果如你所想。如果你要删除一个数组中的元素,请使用 remove
函数,而不是用 nil
赋值。
-- test.lua
local tblTest1 = { 1, a = 2, 3 }
print("Test1 " .. #(tblTest1))
local tblTest2 = { 1, nil }
print("Test2 " .. #(tblTest2))
local tblTest3 = { 1, nil, 2 }
print("Test3 " .. #(tblTest3))
local tblTest4 = { 1, nil, 2, nil }
print("Test4 " .. #(tblTest4))
local tblTest5 = { 1, nil, 2, nil, 3, nil }
print("Test5 " .. #(tblTest5))
local tblTest6 = { 1, nil, 2, nil, 3, nil, 4, nil }
print("Test6 " .. #(tblTest6))
我们分别使用 Lua 和 LuaJIT 来执行一下:
➜ luajit test.lua
Test1 2
Test2 1
Test3 1
Test4 1
Test5 1
Test6 1
➜ lua test.lua
Test1 2
Test2 1
Test3 3
Test4 1
Test5 3
Test6 1
这一段的输出结果,就是这么 匪夷所思。不要在 Lua 的 table 中使用 nil
值,如果一个元素要删除,直接 remove,不要用 nil 去代替。