lua快速入门 —— 迭代器与泛型for

本文最后更新于:2020年1月29日 晚上

概览:Lua的函数的基本形式、如何调用以及函数的多重返回值、变长参数、具名实参。此外还有Lua中函数的地位,即函数是类似于数字那样的第一类值,还有匿名函数、闭包、局部函数以及尾调用函数。

参照书籍:《Lua程序设计(第二版)》

Lua版本:Lua 5.3.5

迭代器与closure

迭代器:一种可以遍历一种集合中所有元素的机制。

在Lua中,通常将迭代器表示为函数,每调用一次函数,就会返回集合中“下一个”元素。

迭代器需要保留上一次成功调用的状态和下一次成功调用的状态,也就是他知道来自于哪里和将要前往哪里。

闭包提供的机制可以很容易实现这个任务。记住:闭包是一个内部函数,它可以访问一个或者多个外部函数的外部局部变量。每次闭包的成功调用后这些外部局部变量都保存他们的值(状态)。当然如果要创建一个闭包必须要创建其外部局部变量。所以一个典型的闭包的结构包含两个函数:一个是闭包自己;另一个是工厂(创建闭包的函数)。

1
2
3
4
5
6
7
function values(t)
local i = 0
return function ()
i = i+1
return t[i]
end
end

上述函数中,values就是一个工厂,每当调用这个工厂时,它就会常见一个新的closure(即迭代器本身)。这个closure将它的状态保存在其外部变量t和i中。每当调用这个迭代器时,它就从列表t中返回下一个值。直到最后一个元素返回后,迭代器就会返回nil,一次表示迭代器的结束。

1
2
3
4
5
6
7
8
9
t = {10,20,30}
iter = values(t) -- 创建迭代器
while true do
local element = iter() --调用迭代器
if element == nil then
break
end
print(element)
end

使用泛型for会更加简单,它正是为这种迭代设计的。

1
2
3
4
t = {10,20,30}
for element in values(t) do
print(element)
end

对比于while循环,泛型for为一次迭代循环做了所有的薄记工作。它在内部保存了迭代器函数,因此不再需要iter变量(while循环中的)。它在每次循环时调用迭代器,并在迭代器返回nil时结束循环。

通常情况下,迭代器函数都难写易用。🙃

泛型for的语义

前面我们看到的迭代器有一个缺点:每次调用都需要创建一个闭包,大多数情况下这种做法都没什么问题,然而在有些情况下创建闭包的代价是不能忍受的。在这些情况下我们可以使用泛型for本身来保存迭代的状态。

泛型for在循环过程内部保存了迭代器函数。实际上它保存了3个值:一个迭代器函数、一个恒定状态和一个控制变量。

1
2
3
4
--泛型for的语法
for <var-list> in <exp-list> do
<body>
end
  • <var-list>是以一个或多个逗号分隔的变量名列表
  • <exp-list>是以一个或多个逗号分隔的表达式列表,通常情况下exp-list只有一个值:迭代工厂的调用。

变量列表的第一个元素称为“控制变量”,当其变为nil时,循环结束。

泛型for的执行过程

首先,初始化,计算in后面表达式的值,表达式应该返回范性for需要的三个值:迭代函数、状态常量、控制变量的初值;与多值赋值一样,如果表达式返回的结果个数不足三个会自动用nil补足,多出部分会被忽略。

第二,将状态常量和控制变量作为参数调用迭代函数(注意:对于for结构来说,状态常量没有用处,仅仅在初始化时获取他的值并传递给迭代函数)。

第三,for将迭代函数返回的值赋给变量列表。

第四,如果返回的第一个值为nil循环结束,否则执行循环体。

第五,回到第二步再次调用迭代函数。

类似于一个do…while循环

无状态的迭代器

无状态的迭代器是指不保留任何状态的迭代器,因此在循环中我们可以利用无状态迭代器避免创建闭包花费额外的代价。

每一次迭代,迭代函数都是用两个变量(状态常量和控制变量)的值作为参数被调用,一个无状态的迭代器只利用这两个值可以获取下一个元素。这种无状态迭代器的典型的简单的例子是ipairs,他遍历数组的每一个元素。

1
2
3
4
a = {"one", "two", "three"}
for i, v in ipairs(a) do
print(i, v)
end

迭代的状态包括被遍历的表(循环过程中不会改变的状态常量)和当前的索引下标(控制变量),ipairs和迭代函数都很简单,我们在Lua中可以这样实现:

1
2
3
4
5
6
7
8
9
10
11
function iter (a, i)
i = i + 1
local v = a[i]
if v then
return i, v
end
end

function ipairs (a)
return iter, a, 0
end

当Lua调用ipairs(a)开始循环时,他获取三个值:迭代函数iter、状态常量a、控制变量初始值0;然后Lua调用iter(a,0)返回1,a[1](除非a[1]=nil);第二次迭代调用iter(a,1)返回2,a[2]……直到第一个非nil元素。

Lua库中实现的pairs是一个用next实现的原始方法:

1
2
3
function pairs (t)
return next, t, nil
end

还可以不使用ipairs直接使用next

1
2
3
for k, v in next, t do
...
end

记住:Lua会自动将for循环中返回结果会被调整为三个,所以Lua获取next、t、nil;确切地说当他调用pairs时获取。

具有复杂状态的迭代器

很多情况下,迭代器需要保存多个状态信息而不是简单的状态常量和控制信息,但泛型for却只提供一个恒定状态和一个控制变量用于状态的保存。

一个最简单的解决办法就是使用closure。🙃(还没想到)

另一个办法是将迭代器所需的所有状态打包为一个table,保存在恒定状态之中。然后迭代器就可以通过这个table就可以保存任意多的数据。此外for还可以在循环过程中改变这些数据。(所谓的恒定是指恒定状态总是那个table)。因为这种情况下可以将所有的信息存放在table内,所以迭代函数通常不需要第二个参数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
-- 便利当前输入文件中的所有单词

local iterator -- to be defined later

function allwords()
local state = {line = io.read(), pos = 1}
return iterator, state
end

function iterator (state)
while state.line do -- repeat while there are lines
-- search for next word
local s, e = string.find(state.line, "%w+", state.pos)
if s then -- found a word?
-- update next position (after this word)
state.pos = e + 1
return string.sub(state.line, s, e)
else -- word not found
state.line = io.read() -- try next line...
state.pos = 1 -- ... from first position
end
end
return nil -- no more lines: end loop
end

我们应该尽可能的写无状态的迭代器,因为这样循环的时候由for来保存状态,不需要创建对象花费的代价小;如果不能用无状态的迭代器实现,应尽可能使用闭包;尽可能不要使用table这种方式,因为创建闭包的代价要比创建table小,另外Lua处理闭包要比处理table速度快些。后面我们还将看到另一种使用协同来创建迭代器的方式,这种方式功能更强但更复杂。

真正的迭代器


本博客所有文章除特别声明外,均采用 CC BY-SA 4.0 协议 ,转载请注明出处!