先看一则示例:

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,来判断返回值是否为空。


spacewander
5.6k 声望1.5k 粉丝

make building blocks that people can understand and use easily, and people will work together to solve the very largest problems.


引用和评论

0 条评论