下面写一下怎么给generic***for ***写迭代器。
## 1. 迭代器和闭包
在Lua中,迭代器用function表示,每次调用该function,都会返回集合中的next元素。
每个迭代器都要在连续的调用之间保存一些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 ***中,但是用generic***for ***更简单:
~~~
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
t = {1, 2, 3}
for element in value(t) do
print(element)
end
~~~
执行结果如下:
![](https://box.kancloud.cn/2016-09-06_57ce5eefa9aeb.PNG)
generic***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
~~~
执行结果如下:
![](https://box.kancloud.cn/2016-09-06_57ce5eefbe309.PNG)
注意执行打印出来的一些信息,可以帮助了解脚本的执行过程。
## 2. Generic***for ***的语义
前面的例子有一个缺陷,每次都需要创建一个新的闭包来初始化一个新的循环。大多数情况下,这不是什么太大的开销。但是,有时也是不能接受的。这时就需要用generic***for ***本身来保存迭代器状态。
Generic***for ***会保存3个值:iterator function, invariant state,control variable。generic***for ***的语法如下:
~~~
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的第一个变量为control variable,它的值为nil时表示循环结束。
***for ***循环先计算in后面的表达式的值,这些表达式会产生3个值,就是上面写的generic***for* **需要保存的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
~~~
如果iterator function为*f* ,invariant state为*s* , control variable的初始值为*a0*,control variable会如下循环:
*a1* =*f* (*s *,*a0 *),*a2* =**f (*s *,*a1 *), *a3* =*f* (*s *,*a2 *), ....
直到ai为nil。
## 3. 无状态迭代器
顾名思义,无状态迭代器自身不会保存任何state。因此,我们可以在多次循环里面用同一个迭代器,避免了创建新闭包的开销。
每次迭代,for 循环用两个参数 invariant state 和 control variable 来调用 iterator 函数。ipairs 是个典型的此类迭代器:
~~~
a = {"one", "two", "three"}
for i, v in ipairs(a) do
print(i, v)
end
~~~
迭代器的state 是被遍历的table ,当前的index 是 control variable 。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 中的第一对值。例如:
![](https://box.kancloud.cn/2016-09-06_57ce5eefd7e58.PNG)
很多人比较喜欢直接用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
~~~
运行结果示例如下:
![](https://box.kancloud.cn/2016-09-06_57ce5ef000aa3.PNG)
我们应该尽可能的使用无状态的迭代器,将所有的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”个数。
执行结果如下:
![](https://box.kancloud.cn/2016-09-06_57ce5ef0185b0.PNG)
这种形式的迭代器在旧版本的Lua中很流行,那是Lua没有*for* 语句。跟“生成器”形式的迭代器比起来,到底哪一种好呢?两种开销差不多,每次迭代都是一次函数调用。从一方面讲,这种旧式迭代器,更容易写。但是“生成器”形式的迭代器更灵活。首先,她可以同时进行两个平行的迭代(例如,比较两个句子中的word,一个一个的比),另外,可以用*break* 在迭代过程中退出。总的来说,“生成器”形式的迭代器也许更受欢迎一点。
水平有限,如果有朋友发现错误,欢迎留言交流。