lua入门笔记2 深入函数 深入函数 迭代器与泛型for 编译、 协同程序(coroutine)执行与错误

这一篇章的内容

  • 深入函数

  • 迭代器与泛型for

  • 编译、执行与错误

  • 协同程序(coroutine)

1.深入函数

  • 基础知识

  1. Lua中,函数是第一类值,和所有其他值一样都是匿名的,即他们都没有名称。当讨论一个函数名时(例如print)实际上是讨论持有某个函数的变量

第一类值:表示Lua中函数与其他传统类型的值(数字、字符串)具有相同的权利。函数可以存储到变量中(无论是全局变量还是局部变量)或table中,可以作为实参传递给其他函数,还可以作为其他函数的返回值。

  1. 函数是由一些表达式创建的,一个函数的定义实际就是一条赋值语句,这条语句创建了一种类型为function的值 ,并把值赋予了一个变量。
 function foo (x) retnrn 2*x end

只是一种语法糖,实际为

foo=functio n x) return 2*x end 
  1. 表达式 function (x) <body> end 可视为一种函数的构造式,就像 table的构造式 {} 一样这种构造使得结果被称为一个 匿名函数 虽然一般情况下 会将函数赋予全局变量, 即给与一个名称 但在某些特殊情况下仍然需要用到匿名函数 例如 table.sort ,需要使用者给出排序的具体方法 如
staff = {
        {age=1,name="a"},
        {age=2,name="b"},
        {age=3,name="c"}
}

可以使用

table.sort(staff,function(a,b) return a.age>b.age end) 

像sort这样可以接受另一个函数作为实参的,便是一个高阶函数。但高阶函数并没有什么特权,lua中强调将函数视为第一类值,所以高阶函数只是一种给予该观点的应用体现而已

  1. 将函数储存在table字段中可以支持许多Lua的高级应用,例如模块(module)和面向对象编程

  • closure(闭合函数、闭包)

  1. 若将一个函数写在另一个函数之内,name这个位于内部的函数便可以访问外部函数中的局部变量,这项特征称之为“语法域” 这里写一个例子
function sortbygrade(name,grades)
table.sort(names,function(n1,n2))
    return grade[n1]>grade[n2]   --比较年级
  end)
end

在这个例子中,内部的匿名函数可以访问参数grades,而grades是外部函数 sortbygrades的局部变量 而在这个匿名函数的内部,grades既不是局部变量也不是全局变量,称之为“非局部变量

简单地讲,一个closure就是一个函数加上该函数所需访问的所有“非局部变量(upvalue)”,即这个韩式以及一系列这个函数会访问到的“非局部变量(upvalue)”

从技术上讲,Lua中只有closure而不存在“函数”。因为函数本身就是一种特殊的closure

closure在另一种情况中也非常有用。例如在Lua中函数是存储在普通变量中的,因此可以轻易地重新定义某些函数,甚至是重新定义那些预定义的函数

下面看一个重新定义函数的例子:重新定义函数sin,使其参数能使用角度来代替原来运行的弧度

do
    local oldSin=math.sin
    local k=math.pi/180
    math.sin=function(x)
       return oldSin(x*k)
    end
end

这段代码将老版本保存在了一个私有变量之中,现在只能用过新版本的sin才能够访问到他。

可以使用同样的技术来创建一个安全的运行环境,即所谓的沙盒(sandbox)当执行一些未受信任的代码就需要一个安全的运行环境,下面我们看一个示例

do 
    local oldOPen=io.open
        local access_Ok=function(filename,mode)
        --《检查访问权限》
         end
        io.open=function(filename,mode)
            if access_OK(filename,mode) then 
              return oldOPen(filename,mode)
            else 
              return nil, "access denied"
            end
         end
end

这个示例的好处是 他在对原方法修改的基础上,并没有改动原方法而是以补丁的形式打了上去,并且使外部无法直接访问到原方法,只能通过新方法通过验证再去调用。相对于提供一套大而全的解决方案,Lua提供的则是一套元机制,因此可以根据特定的安全需求来创建一个安全的运行环境

闭包的本质就是每次运行函数时,都会创建一个closure与一个属于该closure的环境(一个table),当在这个closure中创建一个新的closure,那么这个closure就会继承之前closure的环境,所以数据能够得以保存。

  • 非全局函数

由于函数是一种第一类值,所以函数不仅可以存储在全局变量中,还可以存储在table的字段中和局部变量中。我们之前接触到了几个将函数存储在table中的例子,如io.read math.sin 下面我们看几个示例写法

lib={}
lib.foo=funtion(x,y) return x+y end
--使用构造式
lib={
  foo=function(x,y) return x+y end
}
--另外一种语法定义函数
lib={}
function lib.foo (x,y) return x+y end

只要将一个函数存储到一个局部变量中,即得到了一个局部函数 也就是说该函数只能在某个特定的作用域中使用。
因为Lua是将每个程序块作为一个函数来处理的,所以在一个程序块中声明的函数就是局部函数,这些局部函数只在该程序块中可见。

对于局部函数的定义,Lua还支持一种语法糖

local function f (<参数>)
    <函数体>
end

在定义递归函数时,需要注意将下面是一个错误的示范

local fact =tunction(n)
  if n==0 then return 1
  else return n*fact(n-1)      --错误,此时fact还未完全声明定义结束
  end
end

当lua编译到函数体中调用fact(n-1)的地方时,由于局部变量fact的定义未完毕,因此这句表达式其实是调用了一个全局fact(或报错找不到对象类似的错误)而非函数自身。为了解决该问题,可以先定义一个局部变量。然后在定义函数本身下面是正确示例

local fact                       --正确写法,先声明了fact
fact=function(n)
if n==0 then return 1
   else return n*fact(n-1)
   end
end

由于local function foo(<参数>)<函数体> end只是一种语法糖,展开为

local foo
foo=function(<参数>)<函数体> end

所以 一下递归也是正确的

local function fact(n)
if n==0 then return 1
   else return n*fact(n-1)
   end
end

此种方法只适用于直接递归,对于间接递归(a->b->a->b...)还是需要先声明再定义的方法


  • 正确的尾调用

Lua中还有一个特性是lua支持尾调用消除。 所谓的尾调用就是一种类似于 goto 的函数调用。当一个函数调用时另一个函数的最后一个动作时,该调用才算是一条尾调用。

function f(x) return g(x) end

也就是说,当f调用完g以后f就无事可做了。在这种情况下,程序就不需要返回那个“尾调用”(f(x)) 所在的函数了 当g返回时,执行控制权可以直接返回到调用f的那个点上。是的在进行尾调用 时,不消耗任何的栈空间,这种实现叫做尾调用消除

正确的尾调用消除:不论 n时多大都不会爆栈

function foo (n)
   if n>0 then return foo(n-1) end
end

错误的尾调用消除

function f(x)  g(x) end      --错误完g(x)后需要将g(x)的返回值舍弃
return g(x) +1        --错误,需要做一次加法
return x or g(x)      --错误,需要对g(x)返回值进行计算
return (g(x))       --错误,需要调整g(x)

在lua中 只有 return <func>(<args>)这样的调用形式才算是一条尾调用

之前提到,一条尾调用好比就是一条 goto 语句 因此,在 lua 中 尾调用 的一大引用就是编写状态机 在各个状态切换时 return 该状态函数,可消除尾调用而不会出现递归层数过多爆栈

尾调用消除的实际原因是在每次进行尾调用的时候,都只是完成一条 goto 语句 而非传统调用


  • 迭代器与泛型for

  1. 在Lua中, 通常将迭代器表示为函数,每调用一次函数,及返回集合中的“下一个”元素
  2. 每个迭代器都需要在每次成功调用之间保持一些状态,而一个closure(闭包)就是一种可以访问其外部嵌套环境中的局部变量的函数
  3. 对于closure而言,这些变量就可用于在成功调用之间的状态,从而使closure可以记住他在一次便利中所在的位置
    当然,为了创建一个新的closure还必须创建他的这些 非局部的变量 因此一个closure结构通常涉及到两个函数 closure函数和一个用于创建该closure的工厂(factory)函数

这里写一个简单地迭代器

function values(t)
    local i = 0
    return function() i=i+1; return t[i] end
end

每次调用values时,i变回向后移动一位。
我们这里可以在一个while循环中使用他

--输出a中所有的元素
local a={1,2,3,4,5,6,7,8,9}
local iter=values(a)
while true do 
    local element=iter()
    if element ==nil then break end
    print(element)
end 
  1. 泛型for:
for element in values(t) do 
  print(element)
end

泛型for 为一次迭代循环做了所有的簿记工作,他在内部保存了迭代器函数,因此不再需要iter变量。他在每次新迭代是调用迭代器,并在迭代器返回nil时结束循环

  • 泛型for的语义

在上述的例子中,每一次的迭代都会创建一个closure,这些开销在某些情况下不太容易接受。这是我们希望能通过泛型for的自身来保存迭代器状态.
泛型for 再循环过程内部保存了迭代器函数。实际上他保存着三个值,一个迭代器函数,一个恒定状态,一个控制变量 接下来我们来详细说明这部分
泛型 for 语法如下

for <var-list> in <exp-list>
    <body>
end

其中<var-list>代表一个或多个变量名列表,<exp-list>代表一个或多个表达式的列表 两者都已逗号分隔 通常变量名和列表均只有一个如

for line in io.line() do 
  io.wirte(line,"\n") 
end

变量列表的第一元素称为“控制变量” 在循环过程中该值均不为为nil 因为当他成为nil时循环就结束了

for做的第一件事情是对in后面的表达式求值,子而写表达式会返回三个值供for保存:迭代器函数、恒定状态和控制变量的初值。(这里有点类似于多重赋值,只有最后一个表达式才会产生多个结果并且只会保留前三个值 多于的被丢弃,不足用nil补足)

在初始化步骤以后,for会以恒定状态和控制变量来调用迭代器函数。然后for将迭代器函数的返回值赋予变量列表中的变量。如果第一个返回值为nil,那么循环终止
更明确的说,以下下语句

for var_1,...var_n int <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) 一词类推,直到nil结束循环 如果for还有其他变量,那么他们也会在每次调用f后获得额外的值

  • 无状态的迭代器

是一种自身不保留任何状态的迭代器,在许多个循环中使用同一个无状态的迭代去,避免创建新的closure开销
在每次迭代中,for循环都会用恒定状态和控制变量来调用迭代器函数。一个无状态的迭代器可以根据这两个值来为下次迭代生成喜爱个元素,这类迭代器的一个典型例子就是ipairs。
在这里,迭代的状态就是需要遍历的table(一个恒定状态,他不会在循环中改变)及当前的索引值(控制变量) ipairs(工厂)和迭代器都非常简单,我们可以编写一个示例

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

函数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
  <loop body>
end

要注意的是,lua会自动将for循环中表达式列表的结果调整为三个值


  • 具有复杂状态的迭代器

通常得带器需要保存许多状态,可是泛型for熏制提供一个恒定状态和一个控制变量用于状态的保存。一个最简单的解决方法解释使用closure,或者将迭代器所需的所有状态打包成一个table,保存在恒定状态中

推荐阅读更多精彩内容

  • 因为是蔚蓝的天,所以海也是蓝色。因为是白色的云,所以风才会看不见。 离开时落叶飘荡,铺天盖地的金黄。回来时寒风瑟瑟...
    云翻海浪阅读 91评论 0 6
  • 走过多少路, 读过多少句子, 流浪过多少怀抱, 才可以停驻你的心间。 ...
    oh尘埃阅读 261评论 2 5
  • 选基金就像挑选男女朋友一样,最好能常相厮守,通过长期持有,获取丰厚的回报。因此,买基金时,“知己知彼”才是成功的第...
    手账阅读 793评论 0 5