编程小知识之 Lua 长度运算符(#)

2019-12-25 16:31:57 浏览数 (1)

本文讲解了 Lua 中长度运算符(#)的一些知识 (注: 以下讨论基于 Lua 5.3.5 版本)

基础

Lua 中的长度运算符(#)可以用于获取 table 的"长度",举个简单的例子:

代码语言:javascript复制
local t = { 1, 1, 1 }
print(#t) -- 3

但其实对于 table 而言,长度运算符并不等同于获取 table 的"长度",更准确一些的说法应该是获取 table 序列部分的长度,而所谓序列,是指索引为 1 至 n 的集合(中间不能有空元素),以上面的代码为例,表(table) t 就是一个序列, 索引为 1 至 3,所以表(table) t 的长度即为 3.

而对于下面的 表(table) t:

代码语言:javascript复制
local t = { 1, 1, 1, nil }

虽然表(table) t 有 4 个元素(索引为 1 至 4),但是索引 4 为空元素(nil),所以表(table) t 的序列部分索引是 1 至 3,所以表(table) t 的长度仍为 3:

代码语言:javascript复制
local t = { 1, 1, 1, nil }
print(#t) -- 3

在实际开发中,也并不建议在用作序列的 table 中插入空元素(nil),所以一般来讲,能够在用作序列的 table 上正确使用长度运算符(#),并且了解长度运算符(#)的局限性(只能正确作用于序列上)就足够了.


以下内容涉及实现细节,讨论的示例也并不常见,仅想初步了解的朋友可以跳过阅读,否则容易引起混淆

进阶

接着上面的例子,我们再来看下这段代码:

代码语言:javascript复制
local t = { 1, 1, nil, 1 }
print(#t) -- ?

按照之前的理解,似乎输出应为 2(因为表(table) t 的序列部分索引为 1 至 2),但实际上,程序的输出为 4:

代码语言:javascript复制
local t = { 1, 1, nil, 1 }
print(#t) -- 4

原因在于 Lua 的相关实现中,长度是从最大的数组索引处开始查找的,如果发现该处的元素不为空(nil),就直接向后查询.

在上面的例子中, Lua 首先检查 t[4](t 的最大数组索引为 4),发现不是空元素,于是直接向后查询,发现不存在 t[5] 元素,于是便返回了 4(作为 table 的序列长度,下同).

我们接着来,考虑下面代码:

代码语言:javascript复制
local t = { 1, 1, nil, 1, 1, nil }
print(#t) -- ?

按照之前的讲解,现在表(table) t 的最大数组索引处(t[6])为空元素,于是我们应该直接向前查找 t[5],然后发现 t[5] 并不是空元素,于是返回 5.

但实际上,程序的输出为 2:

代码语言:javascript复制
local t = { 1, 1, nil, 1, 1, nil }
print(#t) -- 2

原因在于当 Lua 发现 table 最大数组索引处的元素为空时,是按二分法的方式向前查找的,当发现 t[6] 为空元素之后, Lua 向前查找的元素不是 t[5],而是 t[3],接着发现 t[3] 是空元素,于是从 t[3] 开始继续向前二分查找,最后返回了 2.

接着我们可以来做些练习了:

代码语言:javascript复制
local t = { 1, nil, nil, nil, nil, nil, nil, nil }
print(#t) -- ?

按照上面的解释,我们很容易知道输出应为 1:

代码语言:javascript复制
local t = { 1, nil, nil, nil, nil, nil, nil, nil }
print(#t) -- 1

接着我们进行赋值操作:

代码语言:javascript复制
local t = { 1, nil, nil, nil, nil, nil, nil, nil }
print(#t) -- 1

t[8] = 1
print(#t) -- ?

这时 table 的最大数组索引处(t[8])不为空元素,按照先前的解释,输出会变成 8:

代码语言:javascript复制
local t = { 1, nil, nil, nil, nil, nil, nil, nil }
print(#t) -- 1

t[8] = 1
print(#t) -- 8

我们再进行一次赋值操作:

代码语言:javascript复制
local t = { 1, nil, nil, nil, nil, nil, nil, nil }
print(#t) -- 1

t[8] = 1
print(#t) -- 8

t[9] = 1
print(#t) -- ?

这个时候输出为多少呢?你也许会猜测是 9,但实际上输出为 1 !

代码语言:javascript复制
local t = { 1, nil, nil, nil, nil, nil, nil, nil }
print(#t) -- 1

t[8] = 1
print(#t) -- 8

t[9] = 1
print(#t) -- 1

原因在于我们最后一次的赋值操作因为新建了索引(之前不存在索引 9),继而触发了 table 的 rehash 流程,在这个流程中, Lua 会根据 table 元素的分布重新调整数组的大小,使的最后的输出变为了 1(这里我们不展开 rehash 的流程细节,有兴趣深入的朋友可以看看 Lua 源码中的 rehash 函数).

高级

如果混合使用 table 中的 数组部分 和 hash部分,则长度运算符(#)的结果会更加复杂一些:

代码语言:javascript复制
local t = { 1, 1, 1, 1, [5] = 1, [9] = 1 }
print(#t) -- ?

当 Lua 发现 table 的最大数组索引处不为空元素时,其会继续在 table 的 hash部分 寻找,继而导致上面的输出为 5:

代码语言:javascript复制
local t = { 1, 1, 1, 1, [5] = 1, [9] = 1 }
print(#t) -- 5

另外, hash 部分的查找流程也是二分进行的,这也导致以下代码的输出为 10(而上面代码的输出为 5) :

代码语言:javascript复制
local t = { 1, 1, 1, 1, [5] = 1, [10] = 1 }
print(#t) -- 10

最后一个例子有些隐晦,在此我们仅仅列出结果,有兴趣了解原因的朋友可以看看 Lua 源码中的 luaO_int2fb 和 luaO_fb2int 两个函数:

代码语言:javascript复制
local t = { 1, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, 2 }
print(#t) -- 18

local t = { 1, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, 2 }
print(#t) -- 1
总结

了解 Lua 中长度运算符(#)的作用并不困难,但其中涉及的细节并不简单(有时候甚至有些隐晦),有兴趣深入的朋友可以从 Lua 源码中的 luaH_getn 函数开始探索.

0 人点赞