文章目录
Lua中有一个很重要的知识点就是迭代器,迭代器通常用于对集合进行遍历,在lua中提供了for/while/repeat等循环方式来控制流程,而for循环中又有两种形式,一种是普通的数字循环形式,也就是for v = e1, e2, e3 do block end
的形式;另一种就是泛型迭代器的形式了,为什么说是泛型呢?因为被迭代的值并不像数字循环形式一样是一个number
的变量,而是可以根据具体的类型来进行判断。
这种迭代器应该所有lua程序员都使用过——
--打印数组a的所有值
a = {"one", "two", "three"}
for i, v in ipairs(a) do
print(i, v)
end
以上来自Lua教程中的for循环举例。
这里就是最常使用的迭代器形式,但是ipairs
只能用于数组,那么怎么样才能自定义一个泛型呢?
下面我们就一步一步来了解一下泛型迭代器。
1.泛型迭代模式的两个组成部分
泛型迭代器的官方文档可以参考这里,我这里也简单地说明一下。
泛型迭代器在使用时分为两个部分,一个部分是for循环的迭代控制语句,也就是for <key-values> in <explist> do <body> end
这一部分;另一部分是迭代器,它是被for循环调用的对象,存在于所对应的部分,这一部分中有一个函数,这个函数就是所谓的迭代器。
2.泛型迭代器的形式
2.1 泛型迭代的调用形式
泛型迭代器听起来有点高大上,但是说白了就是下面这个形式:
for var_1, ···, var_n in explist do block end
我第一䀶看到这个形式的时候也是蒙比的,这个var_1,var_n我能看明白,但这个explist是个什么东西?
不要急,跟着我继续往下看。
同样地,在官方文档中说到,泛型迭代器可以写成一个while
的等价形式:
do
local f, s, var = explist --注意这一行
while true do
local var_1, ···, var_n = f(s, var) --这里调用了f函数,将s和var作为参数传了进去
var = var_1
if var == nil then break end
block
end
end
2.2 泛型迭代器的explist参数
有注意到我在代码中标出来 那一行吗?这里提供了三个变量,f
、s
、var
,其中f
是一个函数 ,这个函数的两个变量分别为s
和var
。
联想一下上面的explist
,泛型迭代的另一个部分就呼之欲出了——迭代器,换句话说,这个explist
就是这个for循环语句能实现迭代所需要的几个参数 。
在上面的while
循环里可以看到,要完成这个迭代,需要三个变量:f
、s
、var
,所以我们在使用泛型迭代的时候,也要对应地提供这三个值给我们的for循环,for循环会在循环内部保存这三个对象用于迭代。
而迭代停止的条件也很简单,与while
形式一样,当满足var == nil
的时候,迭代就会停下来。
——别问为什么是三个,因为这就是lua设计的迭代形式。
3.从使用开始,一步一步实现自己的迭代器
如果看到这里,有的同学还觉得很抽象,没有关系,抽象是对的,下面我们通过一些实例一点一点来了解它的使用。
3.1 获取一个迭代函数
我在前面有提到,泛型迭代需要传入三个参数,那么这三个参数是不是可以手动传入?当然是可以的!
Talk is cheap, show me the code. 下面我们来写一个简单的代码。
因为我们还不知道这三个参数分别是什么,只能从代码里推断,那么一开始我们就从lua自带的迭代器来获取。
我们知道pairs
函数可以实现对数组的遍历,并且它使用的也是泛型迭代器方式进行迭代,那么它返回了什么呢?我们可以用一个简单的测试来得到结果。
tab = {"a","b",c = 1}
print(pairs(tab)) -- 输出内容: function: 001F81C0 table: 001FA518 nil
所以可以看出来,pairs实际上返回了三个值,其中一个是function,一个是table,最后一个是nil,这三个值正好与上面while
形式的三个变量f
、s
、var
一一对应,那么我们是不是可以先获取这三个值,再进行迭代呢?当然可以,代码如下:
tab = {"a","b",c = 1}
f,s,v = pairs(tab)
for key, value in f,s,v do
print(key,value)
end
输出结果如下
1 a
2 b
c 1
看到了吗,即使是不直接使用pair
函数,也可以实现遍历,只要我们有这个迭代器函数,就可以实现对一个table的遍历,换句话说,这个函数是针对于所有table都可用的。
3.2 使用迭代函数
聪明的朋友这时候很可能会想,既然while
形式里使用的f
和s
不是同一个对象,那么我们是不是可以复用这个s
?答案当然也是肯定的!我们甚至可以使用f
来运行其他对象。
下面这一段代码展示了如何使用从pairs里获取的迭代器函数迭代其他值的功能:
tab = {"a","b",c = 1}
tab1 = {"5","6",5}
f,s,v = pairs(tab) --获取迭代器函数
for key, value in f,tab,nil do --使用迭代器迭代tab
print("tab1:\t"..key.."\t"..value)
end
print()
for key, value in f,tab1 do --使用迭代器迭代tab1
print("tab2:\t"..key.."\t"..value)
end
print()
print(f({a = 100}))
-------
输出结果:
tab1: 1 a
tab1: 2 b
tab1: c 1
tab2: 1 5
tab2: 2 6
tab2: 3 5
a 100
我们成功地使用一个迭代器函数手动传入三个参数实现了对另一个tab的迭代。
注意上面我没有再使用v,而使用了nil,因为Lua在运行时如果发现不存在指定变量,会自动填充nil,而在这个函数里,变量v为nil不会影响使用。
3.2 自定义迭代函数
现在,我们已经完全知道如何使用一个迭代器函数了,但是上面只是使用了lua自带的迭代器函数,要如何自定义一个我们自己的迭代器函数呢?
在上面,我顺便测试了一下函数f
,函数f
需要传入一个table,然后会输出一个key_value,这一次我们再来调用一下:
tab = {"a","b",c = 1}
tab1 = {"5","6",5}
f,s,v = pairs(tab) --获取迭代器
-- for key, value in f,tab do --使用迭代器迭代tab
-- print("tab1:\t"..key.."\t"..value)
-- end
-- print()
for key, value in f,tab1,nil do --使用迭代器迭代tab1
print("tab2:\t"..key.."\t"..value)
end
print()
print(f(tab1))
print(f(tab1))
print(f({200,100},1))
print(f({200,100},2))
--------------
输出结果
tab2: 1 5
tab2: 2 6
tab2: 3 5
1 5
1 5
2 100
nil
两次调用f(tab1),输出的结果是一样的;而我们给f的第二个参数进行传参,就获得了不一样的结果,说明f的形式很可能是这样的:
function func(var_1,var_2)
return ret_1,ret_2
end
那么我们也依样画葫芦来写一个。
value = 0;
function func(var_1,var_2)
value = value + 1
if value == 1 then
return "value is 1",var_2
elseif value == 2 then
return "value is 2",var_2
elseif value == 3 then
return "value is 3",var_2
else
return nil,var_2
end
end
for key, value in func,nil,nil do
print("func:\t"..key.."\t",value)
end
-------
输出结果
func: value is 1 nil
func: value is 2 value is 1
func: value is 3 value is 2
这时候输出又发生了有意思的变化,我们下一次输出的value,正好是本次的key。
其实这个问题从上面的等价while
循环就能得出答案,因为每一次被传进来的var_2,确实是上一次的var_1。
那么我们是不是可以利用这个性质呢?
下面来改写一下,把我们的value放到函数里,下面这个函数的形参var_2就是我们上面的value
function func(s,var_2)
var_2 = var_2 + 1
if var_2 == 1 then
return var_2,var_2
elseif var_2 == 2 then
return var_2,var_2
elseif var_2 == 3 then
return var_2,var_2
else
return nil,var_2
end
end
for key, value in func,nil,0 do
print("func:\t"..key.."\t",value)
end
---------
输出结果
func: 1 1
func: 2 2
func: 3 3
我们既然知道本次迭代的key会被作为第二个参数传入迭代函数,那么我们就很自然地可以通过这个key来进行控制。
上面这个例子显得没有太大意义,但是如果我们再稍微改写一下,就制作一个可以输出一个左开右闭区间的整数迭代器,代码如下:
function func(max_value,cur_value)
cur_value = cur_value + 1 --每次将cur_value累加
if cur_value <= max_value then
return cur_value,"cur_value : " .. cur_value .."\tmax_value : "..max_value
end
end
for key, value in func,10,0 do --10被作为max_value传入并且保持不变,0作为开始迭代的第一个值传入
print(value)
end
print()
for key, value in func,0,-5 do--0被作为max_value传入并且保持不变,-5作为开始迭代的第一个值传入
print(value)
end
----------
输出结果
cur_value : 1 max_value : 10
cur_value : 2 max_value : 10
cur_value : 3 max_value : 10
cur_value : 4 max_value : 10
cur_value : 5 max_value : 10
cur_value : 6 max_value : 10
cur_value : 7 max_value : 10
cur_value : 8 max_value : 10
cur_value : 9 max_value : 10
cur_value : 10 max_value : 10
cur_value : -4 max_value : 0
cur_value : -3 max_value : 0
cur_value : -2 max_value : 0
cur_value : -1 max_value : 0
cur_value : 0 max_value : 0
这样,就实现了一个左开右闭的迭代器,在我们将(0,10]
传入时,会为我们将从1开始一直到10的所有数值都打印出来,当我们将(-5,0]
传入时,会打印出从-4一直到0的所有数。
3.4 迭代函数的多返回值
在3.3中我们只使用了value这一个值,根据最上面泛型迭代的形式,我们似乎也可以有多返回——不如我们就来试一下。
还是在上面的基础上进行修改,我们把返回值增加到四个:
function func(max_value,cur_value)
cur_value = cur_value + 1 --每次将cur_value累加
if cur_value <= max_value then
--增加了一个left_nums的返回值
return cur_value,"cur_value : " .. cur_value ,"max_value : "..max_value ,"left_nums : "..max_value - cur_value
end
end
for key, value_1 ,value_2,value_3 in func,10,0 do--10被作为max_value传入并且保持不变,0作为开始迭代的第一个值传入
print(value_1 ,value_2,value_3)
end
------
输出结果
cur_value : 1 max_value : 10 left_nums : 9
cur_value : 2 max_value : 10 left_nums : 8
cur_value : 3 max_value : 10 left_nums : 7
cur_value : 4 max_value : 10 left_nums : 6
cur_value : 5 max_value : 10 left_nums : 5
cur_value : 6 max_value : 10 left_nums : 4
cur_value : 7 max_value : 10 left_nums : 3
cur_value : 8 max_value : 10 left_nums : 2
cur_value : 9 max_value : 10 left_nums : 1
cur_value : 10 max_value : 10 left_nums : 0
这样,我们成功地获取了新增的两个返回值,说明这里也是可以接收的,不过第一个值会被用于key再次被传入迭代函数中。
4.迭代器生成函数
在上面的步骤我们已经明白了泛型迭代的使用方式,但是我们的使用还是太复杂了些,f s v
三个参数这样的传值方式也太不优雅了,既然pairs可以直接一个函数完成传参,我们是不是也可以呢?
还是回到lua的多函数返回,我们知道迭代器函数在这里可以最多接收三个参数,那么我们把这几个参数用函数包起来,通过return一次性传进来不就好了吗?
说干就干,上代码:
function get_iter(start_value,max_value)
local function func(max_value,cur_value) --上面的迭代器函数
cur_value = cur_value + 1
if cur_value <= max_value then
return cur_value,"cur_value : " .. cur_value ,"max_value : "..max_value ,"left_nums : "..max_value - cur_value
end
end
return func,max_value,start_value--最后返回for循环需要的三个变量
end
print(get_iter(0,10))--看看输出的到底是什么
for key, value_1 ,value_2,value_3 in get_iter(0,10) do-
print(value_1 ,value_2,value_3)
end
----------
输出结果
function: 00B2CE98 10 0
cur_value : 1 max_value : 10 left_nums : 9
cur_value : 2 max_value : 10 left_nums : 8
cur_value : 3 max_value : 10 left_nums : 7
cur_value : 4 max_value : 10 left_nums : 6
cur_value : 5 max_value : 10 left_nums : 5
cur_value : 6 max_value : 10 left_nums : 4
cur_value : 7 max_value : 10 left_nums : 3
cur_value : 8 max_value : 10 left_nums : 2
cur_value : 9 max_value : 10 left_nums : 1
cur_value : 10 max_value : 10 left_nums : 0
首先我们可以看到print的结果一共有三个,分别是function\10\0,这个顺序跟我们在上面手动地传入时是一样的。
然后是迭代的结果,跟我们在上面手动传入的结果也是一样的。
这样,我们就实现了一个自己的迭代器生成函数。
5.带状态的迭代函数
我们再回到上一节,熟悉闭包的朋友们肯定很眼熟,这不就是闭包最常见的形式吗?如果我在get_iter()
中增加一个变量,那这个变量就会被存在闭包中作为一个状态使用,那么我们不就获得了一个自带状态的迭代器了吗?对应地,上面那种没有状态的迭代器被叫作无状态的迭代器。
还是在上面的区间输出迭代器上进行修改,我们现在不满足原来的条件了,我们还要知道每个数是第几次输出,如果是普通的迭代器,我们只能在for循环中再加一个变量来控制,显得不够优雅:
function get_iter(start_value,max_value)
local function func(max_value,cur_value)
cur_value = cur_value + 1
if cur_value <= max_value then
return cur_value,cur_value
end
end
return func,max_value,start_value
end
i = 0 --增加一个计数变量
for key,value in get_iter(55,57) do
i = i + 1
print(value,i)
end
-------
输出结果
56 1
57 2
但是我们有闭包了,我们完全可以把这个i放到get_itor
中去。
再次改写代码:
function get_iter(start_value,max_value)
local counter = 0 --增加一个计数变量,每次都会进行初始化为0
local function func(max_value,cur_value)
cur_value = cur_value + 1
counter = counter + 1 --自增
if cur_value <= max_value then
return cur_value,cur_value,counter
end
end
return func,max_value,start_value
end
for key,value,counter in get_iter(55,57) do
print(value,counter)
end
------
输出结果
56 1
57 2
把i放到迭代器生成函数之中,然后让迭代器来进行管理,完全免去了在for循环中增加变量的烦恼,在使用for循环的时候变得更优雅了。
但是使用闭包也不是没有代价的,还记得我们在3.2的时候反复调用迭代器函数,输出的结果都是一样的,但是如果使用闭包获取迭代器,闭包中的值就会成为这个迭代器的状态,会持续发生变化 。
function get_iter(start_value,max_value)
local counter = 0
local function func(max_value,cur_value)
cur_value = cur_value + 1
counter = counter + 1
if cur_value <= max_value then
return cur_value,cur_value,counter
end
end
return func,max_value,start_value
end
func = get_iter(10,12)
print(func(5,2))
print(func(5,2))
print(func(5,2))
print(func(5,2))
print(func(5,2))
--------
输出结果
3 3 1
3 3 2
3 3 3
3 3 4
3 3 5
虽然我们每次传入的都是一样的值,但是我们的计数器却在每次调用中发生变化。
6.总结
在上面的过程里一步一步地从泛型迭代的使用到自定义迭代函数再到封装迭代器生成器,最后是自带状态的迭代器将迭代器的使用方式由简单到复杂地梳理了一遍,希望能对学习到这一部分的朋友们起到一定帮助。