1. 迭代器和闭包
在Lua中,迭代器用函数表示,每次调用该函数,都会返回集合中的下一个元素。
每个迭代器都要在连续的调用之间保存一些state,这样才能知道进行到哪一步,下一步该从哪开始处理。在Lua中,闭包可以处理这个问题。闭包结构包含两个function:一个是闭包本身,另一个是factory,用来创建闭包。下面是个简单的示例:
function values(t)
local i = 0
return function () i = i + 1; return t[i] end
end
在上面的例子中,values是factory。每次调用factory,都会创建一个新的闭包(迭代器本身)。该闭包在变量t和i中保存state。每次调用这个迭代器,都会从t中返回next元素的值。返回最后一个元素后,它会返回nil,标志着迭代器结束。
可以将上面的迭代器用在while 中,但是用genericfor 更简单:
t = {10, 20, 30}
function value(t)
local i = 0
return function () i = i + 1; return t[i] end
end
iter = value(t) -----创建迭代器
while true do
local element = iter() ---------调用迭代器
if element == nil then break end
print(element)
end
------你会发现for是胃迭代器而生的 ,用起来更为简单
t = {1, 2, 3}
for element in value(t) do
print(element)
end
打印结果为:10、 20 、30、 1 、2 、3、
for 为迭代器循环过程在内部保存state,不需要iter变量;每次迭代都调用该迭代器,并在迭代器返回nil时停止循环
下面是一个复杂点的例子,从当前输入的文件中遍历所有的word。遍历过程中保存两个值:当前行(变量line),当前位置(pos),string.find 函数从当前行中搜索一个word,用'%w+'匹配,在匹配到word后,将当前位置pos置于该word之后的第一个字符处,并返回该word。否则,迭代器读入一个新行再重复上面的过程。如果没有更多的行,返回nil,标志迭代结束。
function allwords ()
local line = io.read() --当前行
local pos = 1 --一行中当前的位置
print("allwords begin")
return function () --迭代器函数
while line do -- 若为有效的行内容进去循环
local s, e = string.find(line, "%w+", pos)
if s then --是否找到一个单词
pos = e + 1 --改单词的下一个位置
print("return the word")
return string.sub(line, s, e) -- 返回该单词
else
print("read next line")
line = io.read() --没有找到改单词,尝试下一行
pos = 1 -- 在第一个位置上重新开始
end
end
print("return nil, iter end")
return nil --没有其余行可遍历,结束
end
end
for word in allwords() do
print(word)
end
2.泛型for的语义
前面的例子有一个缺陷,每次都需要创建一个新的闭包来初始化一个新的循环。大多数情况下,这不是什么太大的开销。但是,有时也是不能接受的。这时就需要用genericfor 本身来保存迭代器状态。
Genericfor 会保存3个值:一个迭代器函数(iterator function),一个恒定状态( invariant state)和一个控制变量(control variable)。genericfor 的语法如下:
for <var-list> in <exp-list> do
<body>
end
这里,var-list是一个或多个变量的名字,用逗号分隔,exp-list是一个或多个表达式,也用逗号分隔。通常exp-list只有一个元素,对iterator factory的调用。例如:
for k, v in pairs(t) do print(k, v) end
var-list是k,v, exp-list是pairs(t)。通常,var-list也只有一个变量,如下:
for line in io.lines() do
io.write(line, "\n")
end
var-list的第一个变量为控制变量,它的值为nil时表示循环结束。
for 循环先计算in后面的表达式的值,这些表达式会产生3个值,就是上面写的genericfor 需要保存的3个值:iterator function, invariant state,control variable 的初始值。跟多赋值语句一样,只有最后一个表达式可以产生多个值;但是所有的表达式的结果最终被调整为3个,多的丢弃,少的补nil (当使用简单的迭代器的时候,factory只返回iterator function,invariant state和control variable的值为nil)。
完成上面的初始化步骤后,for 用invariant state和control variable作为参数来调用iterator function。然后for 将iterator function返回的值赋值给var-list中的变量,如果返回的第一个值为nil,那么循环结束。否则,for 执行循环体代码并再次调用iteration function,重复上面的过程。
下面的伪代码也许能帮助理解:
for var_1, ..., var_n in <explist> do <block> end
等价于:
do
local _f, _s, _var = <explist>
while true do
local var_1, ... , var_n = _f(_s, _var)
_var = var_1
if _var == nil then break end
<block>
end
end
如果迭代器函数为f ,恒定状态为s , 控制变量的初始值为a0,那么循环过程中控制变量依次为如下:
a1 =f (s ,a0 ),a2 =f (s , a1 ), a3 =f (s ,a2 ), ....
直到ai为nil结束循环。如果for还有其他变量,那么他们会每次调用f后获得额外的值。
3. 无状态迭代器
顾名思义,无状态迭代器自身不会保存任何状态的迭代器。因此,我们可以在多次循环里面用同一个迭代器,避免了创建新闭包的开销。
每次迭代,for 循环用两个参数恒定状态 和 控制变量 来调用 迭代器 函数。ipairs 是个典型的此类迭代器:
a = {"one", "two", "three"}
for i, v in ipairs(a) do
print(i, v)
end
迭代器的状态 是被遍历的table ,当前的索引值(index)是 控制变量 。ipairs (工厂)和 iterator(迭代器)实现都比较简单,下面是一个示例:
local 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
在for 循环中,Lua调用 ipairs(a) ,得到3个值: iter 函数为iterator , a 为invariant state , 0 是control variable 的初始值。 然后,Lua调用 iter(a,0), 得到1, a[1] 。下一次迭代,调用 iter(a,1), 得到 2, a[2],直到第一个nil值,迭代结束。
pairs 函数跟上面的 ipairs 类似,只是iterator 函数为 next, 不是iter。next 是Lua中的一个基本函数:
function pairs (t)
return next, t, nil
end
next(t, k) 调用,k 是 table t 中的一个key ,返回两个值,next key 和 t[next key],next(t, nil)返回 table 中的第一对值。例如:
t={1,12,3,5}
function pairs (t)
return next, t, nil
end
print(next(t,nil)) -- 1 1返回t中的第一组值
print(next(t,1)) -- 2 12
很多人比较喜欢直接用next ,而不是用pairs:
for k, v in next, t do
<loop body>
end
for 循环的exp-list会被调整为3个值,Lua会得到next,t,nil,跟调用pairs(t)得到的结果是一样的。
下面是一个遍历链表的例子,代码有点绕,仔细想一下就会明白的。
local function getnext (list, node)
return not node and list or node.next
end
function traverse (list)
return getnext, list, nil
end
list = nil
for line in io.lines() do
list = {val = line, next = list}
end
for node in traverse(list) do
print(node.val)
end
4. 复合状态的迭代器
很多时候,需要保存的state,除了 invariant state 和 control variable 还有别的。这种情况,用闭包是最简单的方案。还有一个可选方案,用table,把所有需要保存的东西都打包到table 中,想保存什么就把什么打包到table 中。在循环过程中,尽管state 始终是同一个table,但是table中的数据可以在循环过程中更改。迭代器把所有需要的数据到封装到table 中,所有就忽略了generic for 提供的第二个参数。
下面把allwords那个示例改写了一下,来展示一下上面所说的用法
local iterator
function allwords()
local state = {line = io.read(), pos = 1}
return iterator, state
end
function iterator(state)
while state.line do
--search for next word
local s, e = string.find(state.line, "%w+", state.pos)
if s then
-- update next position (after this word)
state.pos = e + 1
return string.sub(state.line, s, e)
else
state.line = io.read()
state.pos = 1
end
end
return nil
end
for word in allwords() do
print(word)
end
我们应该尽可能的使用无状态的迭代器,将所有的state都保存到for 循环的变量中。在开始新的循环的时候就不需要创建新的对象。当你的迭代器不适合上面说的table的情况时,应该用闭包。用闭包更简洁优美,并且效率更高,因为:1. 创建闭包比创建table开销要小。 2. 访问非局部变量(闭包中)比访问table的域要更快。 后面还会讲讲用coroutines 来实现迭代器,更强大,但是代价也更高。
5. 真正的迭代器
“迭代器”这个名字其实有点误导,因为我们的“迭代器”根本不做迭代操作,真正去做迭代操作的是 for 循环。“迭代器”只是为迭代过程提供连续的值。可能更好的名字应该叫“生成器”。
不过,下面展示一种真正去做迭代操作的“迭代器”,再把allwords 重写一次:
function allwords(f)
for line in io.lines() do
for word in string.gmatch(line, "%w+") do
f(word) -- call the function
end
end
end
allwords(print)
print("=============================")
local count = 0
allwords(function(w) if w == "hello" then count = count + 1 end end)
print("the number of 'hello' is " .. count)
示例中 的参数f 可以传相关的函数,示例中简单的用了
print
来显示了一下句子中的word,用了一个匿名函数统计句子中的“hello”个数。
这种形式的迭代器在旧版本的Lua中很流行,那是Lua没有for 语句。跟“生成器”形式的迭代器比起来,到底哪一种好呢?两种开销差不多,每次迭代都是一次函数调用。从一方面讲,这种旧式迭代器,更容易写。但是“生成器”形式的迭代器更灵活。首先,她可以同时进行两个平行的迭代(例如,比较两个句子中的word,一个一个的比),另外,可以用break 在迭代过程中退出。总的来说,“生成器”形式的迭代器也许更受欢迎一点。