第七课 迭代器与泛型for

来源:互联网 发布:sql having where同时 编辑:程序博客网 时间:2024/05/20 02:51
迭代器就是一种可以遍历一种集合中所有元素的机制。
在Lua中,通常将迭代器表示为函数。每调用一次函数,即返回集合中的“下一个”元素。
每个迭代器都需要在每次成功调用之间保持一些状态,这样才能知道它所在的位置及如何步进到下个位置。closure对于这类任务提供了极佳的支持,一个closure就是一种可以访问其外部嵌套环境中的局部变量的函数。
为了创建一个新的closure,还必须要创建它的这些“非局部的变量”。因此一个closure结构通常涉及到两个函数:closure本身以及一个用于创建该closure的工厂函数。如为一个列表 编写一个简单的迭代器。与ipairs不同的是该迭代器并不是返回每个元素的索引,而是返回元素的值:
function values(t)
local i = 0
return function() i = i + 1; return t[i] end
end
values就是一个工厂。
t = {10, 20, 30}
iter = values(t) --创建迭代器
while true do
local element = iter() --调用迭代器
if element == nil then break end
print(element)
end
使用泛型for则更为简单。接下来会发现,它正是为这种迭代而设计的:
t = {10, 20, 30}
for element in values(t) do
print(element)
end
泛型for为每次迭代循环做了所有的 簿记工作。它在内部保存了迭代器函数,因此不再需要iter变量。它在每次新迭代时调用迭代器,并且在迭代器返回nil时结束循环。
高级的迭代器示例:一个可以遍历当前输入文件中所有单词的迭代器---allwords。(一行一行的处理)
function allwords ()
local line = io.read() --当前行
local pos = 1 --一行中的当前位置
return function ()
while line do
local s, e = string.find(line, "%w+", pos) --使用模式匹-----配%w+匹配一个或多个的文字/数字字符。
if s then --是否找到一个单词
pos = e + 1 --该单词的下个位置
return string.sub(line, s, e) --返回该单词
else
line = io.read() --没找到单词,继续下一行
pos = 1 --在第一个位置重新开始
end
end
return nil --没有新行,结束遍历
end
end
调用allwords迭代器很简单:
for word in allwords() do
print(word)
end

泛型for的语义
上述的那些迭代器都有一个缺点,就是需要为每个新的循环创建一个新的closure。某些情况下,这些开销就不太容易接受了。因此,在这类情况中,希望通过泛型for的自身来保存迭代器状态。
泛型for在循环过程内部保存了迭代器函数。实际上它保存了3个值:一个迭代器函数、一个恒定状态和一个控制变量。
泛型for的语法如下:
for <var-list> in <exp-list> do
<body>
end
其中,<var-list>是一个或多个变量名的列表,以逗号分割;<exp-list>是一个或多个表达式的列表,同样以逗号分割。通常表达式列表只有一个元素,即一句对迭代器工厂的调用。如:
for k, v in pairs(t) do print(k, v) end
其中,变量列表是“k, v”,表达式列表只有一个元素pairs(t)。一般来说变量列表中也只有一个变量,如:
for line in io.lines() do
io.write(line, "\n")
end
变量列表的第一元素称为“控制变量”。在循环过程中该值绝不会为nil,因为当它为nil时循环就结束了。
for做的第一件事就是对in后面的表达式求值。这些表达式应该返回3个值供for保存:迭代器函数、恒定状态和控制变量的初值。这里有点类似与多重赋值,即只有最后一个表达式才会产生多个结果, 并且只会保留前3个值,多余的值会被丢弃;而不足的话,将以nil补足。
在初始化步骤之后,for会以恒定状态和控制变量来调用迭代器函数。然后for将迭代器函数的返回值赋予变量列表中的变量。如果第一个返回值为nil,那么循环终止。否则,for执行它的循环体,随后再次调用迭代器函数,并重复这个过程。
更明确地说,以下语句:
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),依次类推,直到ai为nil结束循环。如果for还有其他变量,那么它们也会在每次调用f后获得额外的值。

无状态的迭代器
一种自身不保存任何状态的迭代器。因此,我们可以在多个循环中使用同一个无状态的迭代器,避免创建新的closure开销。
在每次迭代中,for循环都会用恒定状态和控制变量来调用迭代器函数。一个无状态的迭代器可以根据这两个值来为下次迭代生成下一个元素。这类迭代器的一个典型的例子就是ipairs,它可以用来迭代一个数组的所有元素:
a = {"one", "two", "three"}
for i, v in ipairs(a) do
print(i, v)
end
在这里,迭代的状态就是需要遍历的table(一个恒定状态,它不会在循环中改变)以及当前的索引值(控制变量)。ipairs(工厂)和迭代器都非常简单,在Lua中就可以编写出来:
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
当Lua调用for循环中的ipairs(a)时,它会获得3个值:迭代器函数iter、恒定状态a和控制变量的初值0。然后Lua调用iter(a, 0),得到1, a[1],在第二次迭代中,继续调用iter(a, 1),得到2, a[2],依次类推,直到得到第一个nil元素为止。
函数pairs与ipairs类似,也是用于遍历一个table中的所有元素。不同的是,它的迭代器函数是Lua中的一个基本函数next。
function pairs (t)
return next, t, nil
end
在调用next(t, k)时,k是table t的一个key。此调用会以table中的任意次序返回一组值:此table的下一个key,以及这个key所对应的值。而调用next(t, nil)时,会返回table的第一组值。若没有下一组值时,next返回nil。
有些用户喜欢不通过pairs调用而直接使用next:
for k, v in next ,t do
<body>
end
记住,Lua会自动将for循环中表达式列表的结果调整为3个值。因此上例中得到 了next、t和nil,这也正与调用pairs(t)的结果完全一致。

具有复杂状态的迭代器
通常, 迭代器需要保存许多状态,可是泛型for却只提供的一个恒定状态和一个控制变量用于状态的保存。一个最简单的解决方法就是使用closure。或者还可以将迭代器所需要的所有状态打包为一个table,保存在恒定状态中。一个迭代器通过这个table就可以保存任意多的数据了。此外,它还能在循环过程中改变这些数据。虽然在循环过程中,恒定状态总是同一个table(故称之为恒定),但这个table的内容却可以发生改变。由于这种迭代器可以在恒定状态中保存所有数据,所以他们通常可以忽略泛型for提供的第二个参数(控制变量)。
重写allword迭代器,将它的状态保存到一个table中,这个table具有两个字段:line和pos。
local iterator --在后面定义
function allwords ()
local state = {line = io.read(), pos = 1}
return iterator, state
end

function iterator (state)
while state.line do
local s, e = string.find(state.line, "%w+", state.pos)
if s then
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变量中,不需要在开始一个循环时创建任何新的对象。如果迭代器无法套用这个模型,那么就应该尝试使用closure。closure显得更加优雅点,通常一个基于closure实现的迭代器会比一个使用table的迭代器更加高效。这是因为,首先创建一个closure就比创建一个table更廉价,其次访问“非局部的变量”也比访问table字段更快。以后会看到另一种使用协同程序编写迭代器的方式,这种方式是功能最强的,但稍微有一点开销。

真正的迭代器
“迭代器”这个名称多少有点误导的成分。因为迭代器并没有做实际的迭代,真正做迭代的是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
使用这个迭代器时,需要传入一个描述循环体的函数。例如,只想打印每个单词,那么可以使用print:
allwords(print)
通常,还可以使用一个匿名函数作为循环体。例如,以下代码计算了单词“hello”在输入文件中出现的次数:
local count = 0
allwords(function (w)
if w == "hello" then count = count + 1 end
end)
print(count)
"真正的迭代器"在老版本Lua中曾非常流行,那时语言还没有for语句。




















































0 0
原创粉丝点击