lua unpack 陷阱

先看一则示例:

local ary_with_hole = {1, 3, 5, 7, 9}
print(#ary_with_hole) -- => 5,这正是我想要的

ary_with_hole[4] = nil
print(#ary_with_hole) -- => 3,不是 4?!

ary_with_hole[2] = nil
print(#ary_with_hole) -- => 1,更离奇了!

ary_with_hole[1] = nil
print(#ary_with_hole) -- => 0,现在直接变 0 了?

拜 Lua 内部实现上的细节所赐,如果传递的数组中带有 nil 值空洞,# 操作符返回的数值并不能反映真实的大小。

直接引用 Lua 5.1 manual 上的说法(Lua 5.2 和 LuaJIT 也是一样的定义):

https://www.lua.org/manual/5.... The Length Operator

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. ...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).

如果让我来编写文档,我一定会把上面一段话加粗、调大字号、标红,因为这个实在太坑了。

简单说,Lua 里面 table 的长度的定义跟其他语言的不同。table 的长度,被定义成第一个值为 nil 的整数键(而不是像通常认为那样,等价于元素的数量)。
如果一个 array-like table 里面存在空洞,那么任意 nil 值前面的索引都有可能是 # 操作符返回的值。

在示例中,之所以会有 5 3 1 0 这样的递减,是因为 Lua 在找 nil 值时,大致采用的是从后往前半分查找的方式。如果改变 nil 值的位置,显示的结果则大相径庭。
总而言之,带 nil 的数组的“长度”并不能反映里面元素的数量。

几天前,项目里出了一个需要线上救火的问题,经追查就是因为 unpack 一个带 nil 数组导致的。
unpack(table) 返回的值是 table[1], ..., table[#table]。如果 #table 返回的值不等价于实际的元素数量,unpack 操作返回的值就会被截断。
这显然会导致意料之外的后果。

受带 nil 数组的长度影响的,除了 #unpack 操作,还包括 table.getnipairs 等。

解决方法有二:

  1. 改变对可能会带有 nil 的数组的处理方式。把这样的数组,当作键刚好是整数的 record-like table 去处理。比如改用 pair 而非 ipair 来迭代它。
    这么做有个限制。由于 Lua 是动态语言,而且不提供类型标记之类的语法,要分辨哪些函数返回的是“可能带 nil 的数组”,除了在注释中注明,好像也没别的法子。事实上,除非项目中的老司机做好 code review,不然总免不了会有不了解内情的新人掉坑。

  2. 采用 NullObject pattern,对于空数据,不采用 nil,而是采用其他约定俗成、因地制宜的空值。这里有两个比较适合的候选:false 或 ngx.null。
    前者跟 nil 一样,也是个假值。所以调用代码可以依旧保持 res or default_value 这样的方式,而无需变动。如果 false 也是可能的返回值,则可以用 ngx.null。不过要注意 ngx.null 是一个真值,调用代码需要判断返回的值是否等于 ngx.null,来判断返回值是否为空。

你可能感兴趣的:(lua unpack 陷阱)