迭代器 与泛型for

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 (,a0 ),a2 =f (a1 ), a3 =f (,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 在迭代过程中退出。总的来说,“生成器”形式的迭代器也许更受欢迎一点。






  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值