lua-5.1.5

疑问

lua获取table长度的接口有很多:

  • table.getn()
  • table.maxn()
  • ’#’操作符

推荐一篇文章:浅析Lua中table的遍历

但是也会出现神奇的情况,请看下面一小段代码:

local t = {111, x = 222, nil, 333, [10] = 555, {}, nil, nil}
print(table.getn(t))    -- 4
print(#t)               -- 4
print(table.maxn(t))    -- 10
--local t = {111, nil, x = 222, nil, 333, [10] = 555, {}, nil, nil}
--local t = {111, x = 222, nil, 333, [10] = 555, nil, {}, nil, nil}

如果随机在t中插入nil,结果会让你大吃一惊,丈二和尚摸不着头脑。比如:

  • 在111后面插入一个nil,结果是: 1, 1, 10
  • 在555后面插入一个nil,结果却是:5, 5, 10

这是为什么呢?有没有规律呢?

table的#操作符

这里原因我先简单介绍一下,想弄更清楚的请自己看完之后看源代码,算是抛砖引玉^_^!上面的结果千奇百怪主要是和#操作符有关,#操作符最终获取table长度的函数是(文件ltable.c中):

/*
** Try to find a boundary in table `t'. A `boundary' is an integer index
** such that t[i] is non-nil and t[i+1] is nil (and 0 if t[1] is nil).
*/
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 (t->node == dummynode)  /* hash part is empty? */
    return j;  /* that is easy... */
  else return unbound_search(t, j);
}

只看很小的一部分:

while (j - i > 1) {
  unsigned int m = (i+j)/2;
  if (ttisnil(&t->array[m - 1])) j = m;
  else i = m;
}
return i;

unbound_search函数中也有一部分逻辑和上面代码一样

知道二分查找的人花一点时间应该很快能够理解这段代码的意思,从数组[i-j]中二分查找的方式遍历,如果不为ni的话,相当于长度增加了(i=m);如果为nil的话,相当于长度减少了,也举个简单的例子:

local t = {1, 2, nil}
print("#t1 = ", #t1)
--[[
i=0, j=3
(1)m=1, array[m-1]=1不为nil, i=m=1
(2)m=2, array[m-1]=2不为nil, i=m=2
(3)j-i <=1循环结束,return i,结果为2
]]

local t = {1, nil, 2, nil}
print("#t1 = ", #t1)
–[[
i=0, j=4
(1)m=2, array[m-1]=nil, j=m=2
(2)m=1, array[m-1]=1不为nil, i=m=1
(3)j-i <=1循环结束,return i,结果为1
]]

可以看到,ni的位置不确定性,让返回的长度变幻莫测了,这也就是为什么上面看到的结果千奇百怪了 ^_^

总结

  • lua的’#’操作符获取的长度在table中间有nil的情况很不稳定
  • 虽然看到代码还是有一定规律可循的,但我个人认为也不应该把这个规律用来计算这种table的长度
  • 再回来看luaH_getn函数开头的一小段代码解释说明,会更容易理解一点:’Try to find a boundary in table t'. A boundary’ is an integer index such that t[i] is non-nil and t[i+1] is nil (and 0 if t[1] is nil).’

拓展

1.unpack({…})

我们会经常用到这种方式,比如传进来的参数是不定长的’…‘,然后用table的操作符’{}’括起来,变成一个table,然后使用unpack解包。恰好,前两天有个同事跟我们分享了这个使用方式的漏洞。一开始有点吃惊,看完上面的一些分析只会,也就能够明晰许多了。下面是同事的分享。

问题代码

function fnUnPack( ... )
	local p = {...};
	return unpack(p);
end

print(fnUnPack(1, 2, 3));
print(fnUnPack(1, 2, 3, nil ,4));
print(fnUnPack(1, 2, 3, nil ,4, nil));
–结果
1 2 3
1 2 3 nil 4
1 2 3

解决办法

  • (1)使用table的pack和unpack

示例代码:

function fnUnPack1( ... )
	local p = table.pack(...);
	 return table.unpack(p, 1, p.n);
end

版本lua-5.2及以上

  • (2)使用select

示例代码:

function fnUnPack2( ... )
	local p = {...}
	local n = select('#', ...)
	return unpack(p, 1, n);
end

2.版本5.2及以上

  • lua-5.2以上已经没有table.getn和table.maxn接口了