今天看Lua基础里面的迭代器和泛性for语义,感觉很有意思,特别是为什么闭包可以用作泛性for语义的迭代器这一点,需要思考一下,所以记录一下:

首先我们要知道循环过程中范性for在自己内部保存迭代函数,实际上它保存三个值:迭代函数、状态常量、控制变量。然后for语义执行的过程为:
第一,初始化,计算in后面表达式的值,表达式应该返回范性for需要的三个值:迭代函数、状态常量、控制变量;与多值赋值一样,如果表达式返回的结果个数不足三个会自动用nil补足,多出部分会被忽略。
第二,将状态常量和控制变量作为参数调用迭代函数(注意:对于for结构来说,状态常量没有用处,仅仅在初始化时获取他的值并传递给迭代函数)。
第三,将迭代函数返回的值赋给变量列表。
第四,如果返回的第一个值为nil循环结束,否则执行循环体。
第五,回到第二步再次调用迭代函数。

比如我们不用闭包的方式实现一个类似于ipair()的函数可以这样:

function iter (a, i)
    i = i + 1
    local v = a[i]
    if v then
       return i, v
    end
end

function similar_ipairs_normal (a)
    return iter, a, 0
end

如上所示,当我们使用for循环一个table时,可以这样用:

tb = {'one','two','three','four'}
for i,j in similar_ipairs_normal(tb) do
    print(i,j)
end

我们按照范式for语义可以一步一步分析:

  • 第一步:调用一次similar_ipairs_normal (a)函数,很显然,返回iter、a、0分别对应:迭代函数 = iter、状态常量 = a、控制变量 = 0
  • 第二步,将状态常量和控制变量作为参数让迭代函数调用,即iter(a,0)这样返回的结果就是1,v = a[1] = "one"
  • 第三步,将迭代函数返回的值赋值给参数列表,这样就使得参数i = 1, j = "one"
  • 第四步,继续循环,将状态常量和控制变量作为参数让迭代函数调用,即iter(a,1)这样返回的结果就是2,v = a[2] = "two"
  • 第五步:将迭代函数返回的值赋值给参数列表,这样就使得参数i = 2, j = "two"
  • 第六步,继续循环,将状态常量和控制变量作为参数让迭代函数调用,即iter(a,2)这样返回的结果就是3,v = a[3] = "three"
  • 第七步:将迭代函数返回的值赋值给参数列表,这样就使得参数i = 3, j = "three"
  • 第八步,继续循环,将状态常量和控制变量作为参数让迭代函数调用,即iter(a,3)这样返回的结果就是4,v = a[4] = "four"
  • 第九步:将迭代函数返回的值赋值给参数列表,这样就使得参数i = 4, j = "four"
  • 第十步:继续循环,将状态常量和控制变量作为参数让迭代函数调用,即iter(a,4)这时,可以看出i = 4+1 = 5,v = a[5] = nil,这样就使得if v 不成立,就不会进入return i v语句,这时显然返回的值为nil,这样一来,循环就结束了。

如果使用闭包来代替范性for语义的迭代器的话,我们以这样使用:

function similar_ipairs_closure(a)
    i = 0
    return function()
        i = i+1
        local v = a[i]
        if v then
        return i,v
        end
    end
end

如上所示,当我们使用for循环一个table时,可以这样用:

tb = {'one','two','three','four'}
for i,j in similar_ipairs_closure(tb) do
    print(i,j)
end

我们按照范式for语义可以一步一步分析:

  • 第一步:调用一次similar_closure (a)函数,很显然,返回一个内部匿名函数function()(这个function里面存着局部外部变量i = 0、待传入表a),这时分别对应:迭代函数 = iter、状态常量 = nil、控制变量 = nil
  • 第二步,将状态常量和控制变量作为参数让迭代函数调用,即function(nil,nil) = function(),即直接调用匿名函数,这样返回的结果就是1,v = a[1] = "one"
  • 第三步,将迭代函数返回的值赋值给参数列表,这样就使得参数i = 1, j = "one"
  • 第四步,继续循环,将状态常量和控制变量作为参数让迭代函数调用,即function(a,1) = function(),即直接调用匿名函数这样返回的结果就是2,v = a[2] = "two"
  • 第五步:将迭代函数返回的值赋值给参数列表,这样就使得参数i = 2, j = "two"
  • 第六步,继续循环,将状态常量和控制变量作为参数让迭代函数调用,即function(a,2) = function(),即直接调用匿名函数这样返回的结果就是3,v = a[3] = "three"
  • 第七步:将迭代函数返回的值赋值给参数列表,这样就使得参数i = 3, j = "three"
  • 第八步,继续循环,将状态常量和控制变量作为参数让迭代函数调用,即function(a,3) = function(),即直接调用匿名函数这样返回的结果就是4,v = a[4] = "four"
  • 第九步:将迭代函数返回的值赋值给参数列表,这样就使得参数i = 4, j = "four"
  • 第十步:继续循环,将状态常量和控制变量作为参数让迭代函数调用,即function(a,4) = function(),即直接调用匿名函数这时,可以看出i = 4+1 = 5,v = a[5] = nil,这样就使得if v 不成立,就不会进入return i v语句,这时显然返回的值为nil,这样一来,循环就结束了。

根据上面的分析和对比可以知道,实际上闭包可以保持局部外部变量的特点使得范式for循环的状态变量和控制变量在自身内部被保持,这样也就简化了迭代函数的写法,闭包真的是非常好用呀。