lua数组中使用nil值,可能会导致意想不到的结果,浅析其中原因及解决办法。
先看以下lua代码用例
local t = {1, 2, 3, 4, 5, 6}
print("Test1 " .. #t)
t[6] = nil
print("Test2 " .. #t)
t[4] = nil
print("Test3 " .. #t)
t[2] = nil
print("Test4 " .. #t)
我们使用LuaJIT 2.1执行这个用例,结果如下
# luajit test.lua
Test1 6
Test2 5
Test3 3
Test4 1
使用Lua 5.1.4执行这个用例,结果如下
# lua test.lua
Test1 6
Test2 5
Test3 3
Test4 3
可以发现,我们本意是想删除数组中的元素,每次调用完数组长度应该是减1,但测试结果却令人匪夷所思。这是为什么呢。
查看官方手册,这里直接引用 Lua 5.1 manual 上的原话:
The length of a table t
is defined to be any integer index n
such that t[n]
is not nil and t[n+1]
is nil; moreover, if t[1]
is nil, n
can be zero. For a regular array, with non-nil values from 1 to a given n
, its length is exactly that n
, the index of its last value. If the array has "holes" (that is, nil values between other non-nil values), then #t
can be any of the indices that directly precedes a nil value (that is, it may consider any such nil value as the end of the array).
总结就是,对于常规数组,里面从1到n放着一些非空的值的时候,它的长度就精确的为 n。但如果数组中间被掏空,即 nil 值被夹在非空值之间,那么任意 nil 值前面的索引都有可能是 # 操作符返回的值,所以 #t 的结果才这么奇怪。
除了 # 操作符结果异常外,所有受带nil数组的长度影响的操作都有可能出问题,比如说unpack
、table.getn
、ipairs
。
-
table.getn
:同 # 操作符 -
ipairs
:遍历的时候遇到任意 nil 可能导致后面的元素没遍历到 -
unpack
:遇到 nil 返回值会缺失
所以在lua代码中尽量不要在数组中使用nil,值得一提的是,这些无法预估的现象在测试中是相对难发现的,这些代码跑在生产环境中是个不小的隐患。
解决方法有以下:
-
使用table.remove来删除数组元素,取代赋值nil。注意的是,remove之后数组长度发生变化,同时改变的还有数组元素的索引,比如删掉第二个元素后,使用t[2]才能索引到原来的t[3]。有这种指定索引值的场景需要注意一下。
-
对于空数据,不适用nil,使用别的约定的值去替代。ngx_lua里有两个比较适合的候选:false和ngx.null。这种方式实际上就是用了个约定的“占位符”来替代nil,因此数组长度是不变的,索引也不会发生变化。
-
false:在lua里false也是个假值,所以涉及到
if condition
或res or default_value
相关代码也不需要调整。 -
ngx.null:如果false本身可能是个返回值,就考虑用
ngx.null
。但注意ngx.null
是个真值,相关代码可能需要调整,需要自己在代码中判断if res ~= ngx.null
,来判断是否是空数据
-