迭代器是一种可以遍历一种集合中所有元素的机制。在Lua中,迭代器用函数表示,每调用一次函数即返回集合中的“下一个”元素。
每个迭代器都需要在每次成功调用之间保持一些状态,这样才能知道它所在位置及如何到下一个位置。closure为此提供了支持。因此迭代器遍历集合需要一个closure和创建该closure的工厂函数。
例如一个用于遍历数组的工厂函数(返回一个迭代器函数):
function values(t) --t is a table
local i=0
return function () i=i+1;return t[i] end
end
values()是一个工厂,返回的函数用于遍历一个table。每当调用values函数就创建一个新的closure(包括迭代函数和相关的非局部的变量)。
使用values有以下方法:
①while
t={10,20,30}
it=values(t) -- 创建迭代器
while true do
local element=it() --调用迭代器获得集合的值
if element == nil then break
else print(element)
end
end
②泛型for
for element invalues(t) do
print(element)
end
泛型for在内部保存了迭代函数,因此不需要显式地指定一个values返回值(it)。
对于上述while的循环,每次调用迭代器都要创建一个新的闭包(每执行一次closure就不同了???)。在某些情况下,这种开销是不允许的。而泛型for自身保存了迭代器状态。
泛型for循环保存了3个值:迭代器函数、状态常量(invariant state)、控制变量(control variable)。
泛型for的语法如下:
for
end
var-list是一个或多个变量列表。exp-list是一个或多个表达式列表,通常只含一个元素即对工厂函数的调用。
变量列表的第一个元素称为“控制变量”。
泛型for执行过程如下:
①对exp-list求值,得到3个值,即迭代器函数、状态常量、控制变量。exp-list与多重赋值一样,返回多个值时只保留前三个,并且只有最后一个表达式可获得多个结果。不足三个时补nil。
②初始化后用状态常量和控制变量调用迭代器函数——对于 for 结构来说,状态常量没有用处,仅仅在初始化时获取他的值并传递给迭代函数。
③将迭代器函数的返回值赋给var-list。
④如果第一个返回值为nil则结束循环,否则执行循环体。
⑤执行第②步,用新的控制变量(③得到的)作为参数调用迭代函数。
总的来说,用语句表示如下:
for var_1,…,var_nin
等同于
do
local _f,_s,_var=
while ture do
local var_1,…,var_n = _f(_s,_var)
_var = var_1
if _var ==nil then break end
end
end
_f,_s,_var分别表示迭代器函数、状态常量、控制变量。在用_f对变量列表初始化后,将第一个变量赋值给控制变量,当控制变量为nil时结束循环。
正如上一节介绍的泛型for机制,for来遍历table只用到了它自带的三个变量来实现。这种自身不保存任何状态的迭代器称为“无状态的迭代器”。这样可以在多个for循环中使用同一个“无状态的迭代器”,避免创建新的closure。
ipairs就是一个例子:
local functioniter(a,i)
i=i+1
local v=a[i]
if v then
return i,v
end
end
--ipairs函数实现
function ipairs(a)
return iter,a,0
end
ipairs返回了迭代器函数(iter)、状态常量(a,即要遍历的table)、控制变量的初值(0)。通过这三个变量就可以完成对table的遍历。是通过保存table和return i并作为遍历下一个元素的参数传入迭代器函数的方式来遍历的,不需要创建closure。
pairs函数的迭代器函数是一个基本函数next:
function pairs(t)
return next,t,nil
end
调用next(t,k)时,k是table t的一个key,返回任意的一组值:此table的下一个key和对应的value。而调用next(t,nil)时返回第一组值(key-value),没有下一组时next返回nil。
可以直接使用next来遍历,而不用pairs:
for k,v in next,t do --_f=next,_s=t,_var=nil
end
泛型for只能提供状态常量和控制变量来保存状态,不能满足需要保存很多状态的迭代器。这个时候有两个解决方案:closure和table。closure的使用与第一节讲的那样(values例子的closure只保存了i但可以保存更多);而table是指用一个table来保存多个状态。这个table可以修改其中的内容,即更新它保存的状态值,来达到控制遍历(结束条件)。
--table保存多个状态值风格的迭代器
function allwords()
local state={line=io.read(),pos=1}
return iterator,state
end
functioniterator(state)
while state.line do
local s,e=string.find(state.line,"%w+",state.pos)
if s then
state.pos=e+1
returnstring.sub(state.line,s,e)
else
state.line=io.read()
state.pos=1
end
end
return nil
end
io.input("./words.txt")
for word inallwords() do
print(word)
end
虽然state是定义在allwords()函数中的局部变量,但它return了这个局部变量赋给泛型for的_s变量(在调用allwords()时),因此也不存在closure。但是closure比用table更加高效。因为创建一个closure比创建一个table更廉价,并且访问“非局部的变量”比访问table字段更快。
上述的迭代器其实是一个工厂函数,更准确的叫法应该是“生成器(generator)”。本节的“‘真正’的迭代器”是指在一个函数中完成迭代操作,而不是返回一个迭代器函数。也就不需要再用循环语句去调用返回的迭代器函数。这种迭代器接受一个函数作为参数,并在其内部的循环中调用这个函数。
functionallwords(f) --函数作为参数
for line in io.lines() do
for word instring.gmatch(line,"%w+") do
f(word) --在循环中调用函数
end
end
end
allwords(print)--调用迭代器。单纯的打印单词。
也可以使用匿名函数作为参数。
“真正的迭代器”是老版本中的用法,因为那时没有for语句。两者都有相同的开销,但“生成器”更具灵活性——它允许两个或多个并行的迭代过程(例如要逐字比较两个文件,那就需要同时遍历两个文件),其次它允许在迭代中使用break和return。而在真正的迭代器风格写法中 return 语句只是从匿名函数中返回而不是退出循环。
本章是介绍如何设计迭代器和泛型for的原理。迭代器实现主要有
方式 |
说明 |
有无closure |
closure |
|
有 |
泛型for |
泛型for的语义 |
无 |
table |
复杂状态的迭代器用table来保存 |
无 |
“真正”的迭代器 |
迭代器中完成迭代操作 |
无 |