Lua 长度运算符 (#) 与 nil 值

在阅读了这篇文章并进行了一些实验后,我正在尝试了解当一个表中包含 nil 值时,Lua 长度运算符的工作原理。

在我开始调查之前,我认为长度只是从索引 1 开始的连续非 nil 元素的数量:

print(#{nil})         -- 0
print(#{"o"})         -- 1
print(#{"o",nil})     -- 1
print(#{"o","o"})     -- 2
print(#{"o","o",nil}) -- 2

这看起来非常简单,对吧?

但当我在 nil 终止的表中偶然添加了一个元素后,我的头痛就开始了:

print(#{"o",nil,"o"})

我猜它应该打印 1,因为当第一个 nil 被找到时它会停止计数。或者,如果长度运算符足够贪婪,它应该打印 2,查找第一个 nil 之后的非 nil 元素。但上述代码打印 3。

所以我做了几个其他测试来看看会发生什么:

-- 结束之前有 nil
print(#{nil,"o"})     -- 2
print(#{nil,"o","o"}) -- 3
print(#{"o",nil,"o"}) -- 3

-- 几个 nil 元素
print(#{"o",nil,nil}) -- 1
print(#{nil,"o",nil}) -- 0
print(#{nil,nil,"o"}) -- 3

我应该提到的是,repl.it 目前使用的是相当旧的 Lua 5.1.5,但是如果您使用 Lua demo,它目前使用的是 Lua 5.3.5,您将获得相同的结果。

通过观察这些结果并查看这篇答案,我认为:

  • 如果最后一个元素不是 nil,则长度运算符返回表的完整大小,包括任何 nil 条目
  • 如果最后一个元素是 nil,则计算连续的非 nil 元素的数量,并在第一个 nil 处停止计数

这些假设正确吗?

当一个表包含一个或多个 nil 值时,我们能否预测一个 100% 定义良好的行为?

Lua 文档说明 只有当表是一个序列时,才定义了表的长度。这是否意味着长度运算符对非序列具有未定义的行为?

除了长度运算符,nil 值在表中会导致任何麻烦吗?

点赞
用户5287638
用户5287638

length 运算符对于非序列的表(即中间含有 nil 元素的表)产生未定义行为。这意味着即使 Lua 实现始终以某种方式进行操作,您也不应该依赖该行为,因为它可能会在将来的 Lua 版本中或在不同的实现(如 LuaJIT)中更改。

您可以在表中使用 nil。这没有问题,只需不要在可能存在 nil 的表上使用长度运算符来计算非 nil 值的长度。

您所链接的帖子包含有关实际算法的更多细节。它提到用二分查找算法计算元素。这不同于逐个计算元素 - 如果表中存在 nil,则根据它们的确切位置,二分查找算法可能将它们视为表的末尾或忽略它们。

总之,算法比您所认为的更难以预测,即使在任何给定情况下可以技术上预测会发生什么,您也不应该依赖于该行为,因为它可能会更改。

2019-08-06 14:15:41
用户1424244
用户1424244

当一个表是一个序列(所有数字键从1开始且没有“nil”间隙),被定义为这些元素的数量。

对于非序列表,它有点更加复杂。Lua 5.2似乎将结果未定义。对于5.1和5.3,运算的结果就是一个边界。

表中的边界是任何包含非“nil”值后跟“nil”的正索引,如果第一个元素是“nil”,则为0。被定义为返回满足这些条件的任何值。

从另一个角度看,由于表包含“数组”部分和“映射”部分,Lua无法知道“映射”索引从哪里开始。例如,你可以创建一个包含1000个值的表,然后将它们的前999个值设为“nil”;这可能导致你得到一个“大小”为1000的表。然而,你也可以从一个空表开始,并设置第1000个元素,得到一个“大小”为0但在结构上等同于第一个表的表。然后,的结果只是内部算法找到的第一个有效值。

2019-08-07 01:50:11
用户1244588
用户1244588

我们可以预测一些行为,但它不是标准化的,因此您永远不应该依赖它。这个major version的Lua中这种行为很可能会发生变化。

如果你需要用nil值填充一个表,我建议使用一个独特的占位符值来包装表并替换空缺(例如 NIL={};if v==nil then t[k]=NIL,这很容易测试并且安全)。

尽管如此...

由于#结果受表如何定义的影响,您必须区分静态定义(常量)表和动态定义(静音)表。

静态表定义:

#{nil,nil,nil,nil,nil,  1} -- 6
#{3, 2, nil, 1} -- 4

#{nil,nil,nil,  1,  1,nil} -- 0
#{nil,nil,  1,  1,  1,nil} -- 5
#{nil,  1,  1,  1,  1,nil} -- 5
#{nil,nil,nil,nil,  1,nil} -- 0
#{nil,nil,  1,nil,  1,nil,nil} -- 5
#{nil,nil,nil,  1,nil,nil,  1,nil} -- 4

使用这种定义,只要最后一个值不为nil,您将获得等于最后一个值位置的长度。 如果最后一个值为nil,Lua会从尾部开始进行(非线性)搜索,直到找到第一个非nil值。

动态数据定义

local x={}; x[5]=1;print(#x) -- 0
local x={}; x[1]=1;x[2]=1;x[3]=1;x[5]=1;print(#x) -- 3
local x={}; x[1]=1;x[2]=1;x[4]=1;x[5]=1;print(#x) -- 5

#{[5]=1} -- 0
local x={nil,nil,nil,1};x[5]=1;print(#x) -- 0

一旦表被更改,运算符的工作方式就会相反(这包括使用[]的静态定义)。如果第一个元素是nil,#总是返回0,但如果不是,它会开始搜索(我没有深入研究,我猜你可以检查源代码),直到找到一个在非nil 值之前的nil值。

如前所述,依赖这种行为不是一个好主意,并且会导致许多问题。但是,如果您想制作一个混乱的不可维护的程序来干扰同事,那么这是一种可靠的方法。

2019-08-11 16:49:18