小言_互联网的博客

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

384人阅读  评论(0)

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

基础

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

local t = { 1, 1, 1 }
print(#t) -- 3

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

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

local t = { 1, 1, 1, nil }

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

local t = { 1, 1, 1, nil }
print(#t) -- 3

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


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

进阶

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

local t = { 1, 1, nil, 1 }
print(#t) -- ?

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

local t = { 1, 1, nil, 1 }
print(#t) -- 4

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

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

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

local t = { 1, 1, nil, 1, 1, nil }
print(#t) -- ?

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

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

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.

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

local t = { 1, nil, nil, nil, nil, nil, nil, nil }
print(#t) -- ?

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

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

接着我们进行赋值操作:

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

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

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

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

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

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

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 !

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部分,则长度运算符(#)的结果会更加复杂一些:

local t = { 1, 1, 1, 1, [5] = 1, [9] = 1 }
print(#t) -- ?

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

local t = { 1, 1, 1, 1, [5] = 1, [9] = 1 }
print(#t) -- 5

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

local t = { 1, 1, 1, 1, [5] = 1, [10] = 1 }
print(#t) -- 10

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

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 函数开始探索.


转载:https://blog.csdn.net/tkokof1/article/details/103636559
查看评论
* 以上用户言论只代表其个人观点,不代表本网站的观点或立场