Lua函数

作者: JunChow520 | 来源:发表于2018-12-01 18:20 被阅读102次

    函数有两种用途:

    1. 完成指定任务,此时函数作为调用语句使用。
    2. 计算并返回值,此时函数作为赋值语句的表达式使用。
    function fn(arguments-list)
      statements-list
    end
    

    调用函数时,如果参数列表为空,必须使用()表明是函数调用。

    print(os.date())
    

    当函数只有一个参数且参数是字符串或表构造式时,()是可选的。

    print "hello world"
    
    dofile "main.lua"
    
    print [[a multi-line message]]
    
    fn{x=10, y=20}
    
    type{}
    

    在Lua中函数都是function类型的对象

    • 可被比较
    • 可赋值给一个变量
    • 可传递给函数
    • 可从函数中返回
    • 可作为table表中的键

    函数定义

    Lua使用关键字function定义函数

    function fn(arg)
      -- function body...
    end
    

    函数定义的语法会定义一个全局函数,名为fn,全局函数本质上是函数类型的值赋给全局变量。

    函数变量式

    fn = function(arg)
      -- function body...
    end
    

    由于函数定义本质上是变量赋值,变量的定义总是应放置在变量使用之前,所以函数的定义也需要放置在函数调用之前。

    local function fn(arg)
      -- function body...
    end
    
    local fn = function(arg)
      -- function body...
    end
    

    如果参数列表为空,必须使用()表明函数调用。

    定义函数并调用

    -- 定义函数
    function fn()
      print("hello function")
    end
    -- 调用函数
    fn() 
    

    在定义函数时要注意

    • 利用名字来解释函数,变量的目的使人通过名字就能看出函数、变量的作用。
    • 每个函数的长度要尽量控制在一个屏幕内,一样就能看明白。
    • 让代码自己说话,最好是不需要注释。

    由于函数定义等价于变量赋值,因此也可以将函数名替换为Lua表中的某个字段。

    -- 这种形式的函数定义不能使用local修饰符,因为不存在定义新的局部变量了。
    foo.bar = function()
      -- function body...
    end
    
    function foo.bar()
      -- function body ...
    end
    

    案例:接收两个参数,计算加减乘除的结果,并输出到屏幕。

    function fn(i,j)
        return i+j, i-j, i*j, i/j, i%j;
    end
    
    a,b,c,d,e = fn(10,5)
    print(a,b,c,d,e) -- 15  5   50  2.0 0
    

    函数参数

    按值传递

    Lua函数的参数大部分是按值传递的,值传递就是调用函数时,实参把它的值通过赋值运算传递给形参,然后形参的改变和实参就没有关系了。在这个过程中,实参是通过它在参数表中的位置与形参匹配起来的。

    local function swap(x,y)
      local tmp = x
      x = y
      y = tmp
      print(x,y)
    end
    

    在调用函数时,若形参个数和实参个数不同时,Lua会自动调整实参个数。调整规则:
    若实参个数大于形参个数,从左向右,多余的实参被忽略。若实参个数小于形参个数,从左向右,没有实参初始化的形参会被初始化为nil

    变长参数

    https://moonbingbing.gitbooks.io/openresty-best-practices/content/lua/function_parameter.html

    若定义一个函数,参数个数不固定,应该怎么办呢?这就涉及到Lua中函数的可变参数。

    -- Lua中三个点表示函数的参数个数不确定,可以改变,即可变参数。
    function fn(...)
    
    end
    

    return关键字只能出现在语句块的结尾,也就是说,在end之前,或者是else之前,或者是until之前。

    function fn(x)
        return x*x*x;
    end
    
    n = fn(5)
    print(n) -- 125
    
    function fn(x)
        if x<10 then
            return x*x*x
        else
            return x*x
        end
    end
    
    n = fn(5)
    print(n)-- 125
    

    函数基础

    Lua中函数是一种对语句和表达式进行抽象的主要机制。

    Lua中函数即可完成某项特定的任务,一条函数调用可视为一条语句。

    $ lua
    >  a = match.sin(3) + math.cos(10)
    > print(a)
    -0.69795152101659
    
    > print(os.date())
    02/04/18 17:42:52
    

    Lua函数也可以只做一些计算并返回结果,可视为一句表达式。

    $ lua
    > print(8*9, 9/8)
    72      1.125
    

    无论哪种用法都需将参数放入一对圆括号中。即使调用函数时没有参数,也必须写出一对空括号。对此规则只有一种特殊的例外情况:一个函数若只有一个参数,并且此参数是一个字面量字符串或table构造式,那么圆括号便是可有可无的。

    $ lua
    > print "hello world"
    > dofile "test.lua"
    > print [[a multi-line message]]
    

    函数只有一个参数,且参数是一个table构造式,则圆括号可有可无。

    > f{x=10, y=20}
    > f({x=10, y=20})
    
    > type{}
    > type({})
    

    Lua为面向对象式的调用也提供了一种特殊的语法 - 冒号操作符。

    > obj.foo(obj, arg)
    -- 冒号操作符使调用obj.foo时将obj隐含地作为函数的第一个函数
    > obj:foo(arg)
    

    Lua程序即可使用以Lua编写的函数,也可调用以C语言编写的函数。

    function add(params)
        local sum=0;
        for k,v in ipairs(params) do
            sum=sum+v
        end
        return sum
    end
    
    print(add({1,2,3}))
    

    在这种语法中,一个函数定义具有一个名词(函数名)、一些列参数(参数表)、一个函数体(一系列语句)。

    形式参数(parameter)的使用方式与局部变量非常类似,它们是由调用函数时的实际参数(argument)初始化的。

    调用函数时提供的实参数量可与形参数量不同,Lua会自动调整实参数量,以匹配参数表的要求。这项调整与多重赋值(multiple assignment)很相似,即“若实参多于形参,则舍弃多于实参;若实参不足,则多余形参初始化为nil”。

    function fn(a,b)
        print(a,b);
    end
    
    fn(1) -- 1 nil
    fn(1,2) -- 1 2
    fn(1,2,3) -- 1 2
    

    虽然这种调整行为会导致一些编程错误,但它也是很有用的,尤其是对于默认实参的应用。

    function incCount(n)
        n = n or 1 -- 函数以1作为默认实参
        count = count + n
        print(n, count)
    end
    
    fn() -- attempt to perform arithmetic on a nil value (global 'count')
    

    多重返回值

    Lua函数具有一项非常与众不同的特征,允许函数返回多个结果。

    Lua的几个预定义函数就是返回多个值的,string.find()用于在字符串中定位一个模式(pattern),该函数允许在字符串中找到指定的模式,将返回匹配的起始字符和结尾字符的索引。

    startstr,endstr = string.find("hello lua", "lua")
    print(startstr, endstr) -- 7 9
    

    以Lua编写的函数同样可以返回多个结果,只需在return关键字后列出所有的返回值即可。

    例如:查找数组中最大元素并返回该元素的位置

    function max(tbl)
        local index = 1
        local value = tbl[index]
        for i,v in ipairs(tbl) do
            if(v > value) then
                index = i
                value = v
            end
        end
        return index,value
    end
    
    print(max{9,2,12,8,3,9})-- 3 12
    

    Lua会调整函数返回值数量以适应不同的调用情况

    • 若将函数调用作为一条单独语句时,Lua会丢弃函数的所有返回值。
    • 所将函数作为表达式的一部分来调用时,Lua只保留函数的第一个返回值。只有当一个函数调用是“一系列表达式”中最后一个元素或仅有一个元素时,才能获得它的所有返回值。
    -- test
    local tbl = {9,2,12,8,3,9}
    
    local a = max(tbl)
    print(a) -- 3
    
    local a,b = max(tbl)
    print(a, b) -- 3 12
    
    local a,b,c =  max(tbl) 
    print(a,b,c)-- 3 12 nil
    
    local a,b,c = 1, max(tbl) 
    print(a,b,c)-- 1 3 12
    
    local a,b,c,d = 1,max(tbl),0
    print(a,b,c,d) -- 1 3 0 nil
    

    这里所谓的“一系列表达式”,在Lua中表现为4种情况

    • 多重赋值
    • 函数调用时传入的实参列表
    • table的构造式
    • return语句

    在多重赋值中,若函数调用是最后的或仅有的一个表达式,那么Lua会保留其尽可能多的返回值,用于匹配赋值变量。若函数没有返回值或没有足够多的返回值,那么Lua会用nil来补充缺失的值。若函数调用不是一些列表达式的最后一个元素,那么将只产生一个值。

    当函数调用作为另一个函数调用的最后一个或仅有的实参时,第一个函数所有返回值都将作为实参传入第二个函数。

    function fn1()
    end
    
    function fn2()
        return 1
    end
    
    function fn3()
        return 1,2
    end
    
    print(fn1())
    print(fn2()) -- 1
    print(fn3()) -- 1 2
    
    print(fn2(),2) -- 1 2
    print(fn2()..'x') -- 1x
    

    table构造式可以完整地接收一个函数调用的所有结果,即不会有任何数量方面的调整。

    function fn1()
    end
    
    function fn2()
        return 1
    end
    
    function fn3()
        return 1,2
    end
    
    tbl1 = {fn1()} -- 相当于 tbl1={}
    tbl2 = {fn2()} -- 相当于 tbl2={1}
    tbl3 = {fn3()} -- 相当于 tbl3={1,2}
    

    不过这种行为只有当一个函数调用作为最后一个元素时才会发生,而在其他位置上的函数调用总是只缠身给一个结果值。

    function fn1()
    end
    
    function fn2()
        return 1
    end
    
    function fn3()
        return 1,2
    end
    
    tbl = {fn1(), fn2(), fn3(), 4}
    print(tbl[1], tbl[2], tbl[3], tbl[4]) -- nil 1 1 4
    

    最后一种情况是return语句,诸如return fn()这样的语句将返回fn的所有返回值。

    function fn1()
    
    end
    function fn2()
        return 1
    end
    function fn3()
        return 1,2
    end
    function fn(i)
        if i==1 then return fn1()
        elseif i==2 then return fn2()
        elseif i==3 then return fn3()
        end
    end
    
    print(fn(1), fn(2), fn(3)) -- nil 1 1 2
    
    -- 将函数调用放入一对圆括号中,从而迫使它只返回一个结果。
    print((fn(1)), (fn(2)), (fn(3))) -- nil 1 2
    

    注意return语句后面的内容不需要圆括号,在该位置上书写圆括号会导致不同的行为。

    return (fn(3)) -- 只返回一个值,而无关于fn()返回几个值
    

    关于多重返回值还要介绍一个特殊函数 unpackunpack接收一个数组作为参数,并从下标1开始返回该数组的所有元素。

    -- lua5.2+中全局unpack函数已被移除,并被table.unpack替代。
    local unpack = unpack or table.unpack
    
    x,y,z = unpack{1,2,3}
    print(x,y,z) -- 1 2 3
    

    unpack的一项重要用户体现在“泛型调用(generic call)”机制中,泛型调用机制可动态地以任何实参来调用任何函数。

    -- lua5.2+中全局unpack函数已被移除,并被table.unpack替代。
    local unpack = unpack or table.unpack
    
    fn = string.find
    tbl = {'hello lua', 'lua'}
    
    print(fn(unpack(tbl))) -- 7 9
    

    变长参数

    Lua函数可接受可变数量的参数,和C语言类似。在函数参数列表中使用...表示函数有可变的参数。

    Lua将函数的参数放在一个叫做arg的表中,除了参数外,arg表中还有一个域n表示参数的个数。

    function dump(...)
      local str = ""
      for i,v in ipairs(arg) do
        str = str .. tostring(v).."\t"
      end
      return str.."\n"
    end
    

    例如:只想要string.find返回的第二个值,典型的方法是使用虚变量_

    local _, x = string.find(str, pattern)
    

    Lua中的函数还可以接受不同数量的实参

    例如:返回所有参数的总和

    function sum(...)
        local sum=0
        for i,v in ipairs{...} do
            sum = sum + v
        end
        return sum
    end
    
    print(sum(1,2,3,4)) -- 10
    

    参数中3个点...表示函数可接收不同数量的实参,当函数被调用时,它的所有参数都会被收集到一起。这部分收集起来的实参称为函数的“变长参数(variable arguments)”。函数要访问它的变长参数时,仍需要3个点。但不同的是,此时这个3个点是作为一个表达式来使用的。

    function fn(...)
        local x,y,z = ...
        print(x,y,z)
    end
    fn(1,2) -- 1 2 nil
    fn(1,2,3) -- 1 2 3
    

    表达式...的行为类似于一个具有多重返回值的函数,它返回的是当前函数的所有变量参数。

    function fwrite(fmt, ...)
        return io.write(string.format(fmt, ...))
    end
    fwrite("%s %s %s", 1, 2, 3) -- 1 2 3
    

    Lua中迭代函数参数时,可使用...参数收集到一个table中,但变参中函数非法的nil时,可使用select函数将其过滤掉。

    function filter(...)
        for i=1, select("#", ...) do
            local arg = select(i, ...)
            if arg ~= nil then
                print(arg)
            end
        end
    end
    -- test
    test(1,nil,2,3)-- 1 2 3
    

    具名实参

    Lua的函数参数是和位置相关的,调用时会按顺序依次传递给形式参数。有时候,使用名字制定参数是很有用的(命名参数)。

    Lua中的参数传递机制和是具有“位置性”的,也就是说在调用函数时,实参时通过它在参数表中的位置与形参匹配起来。

    function rename(tbl)
        print(tbl, tbl.oriname, tbl.newname)
    end
    rename{oriname="ori.lua", newname="new.lua"}
    

    Lua中特殊的函数调用语法,当实参只有一个table构造式时,函数调用中的圆括号是可有可无的。

    function rename(arg)
      return os.rename(arg.old, arg.new)
    end
    

    第一类值

    Lua中的函数是带有词法定界(lexical scoping)的第一类值(first-class values)。

    "第一类值"是指在Lua中函数和其他值(数值、字符串...)一样,函数可以被存储在变量中,可以存放在表中,可以作为函数的参数,也可以作为函数的返回值。

    “词法界定”是指被嵌套的函数可以访问其他外部函数中的变量,这一特性给Lua提供了强大的编程能力。

    Lua中关于函数难以理解的是“函数是可以没有名字的,也就是匿名的。”,但提到函数名时,实际上是说以一个指向函数的变量,和其他类型值得变量是一样的。

    Lua中函数作为“第一类值”,表示函数可以存储在变量中,可通过参数传递给其他函数,还可作为其他函数的返回值。由于函数在Lua中是一种“第一类值”,所以不仅可将其存储在全局变量中,还可存储在局部变量甚至table的字段中。

    Lua中函数是一种“第一类值(First-Class Value)”,它们具有特定的词法域(Lexical Scoping)。

    “第一类值”是什么意思呢?这表示在Lua中函数与其他传统类型的值具有相同的权利。函数可存储到变量中或table中,可作为实参传递给其他函数,还可作为其他函数的返回值。

    “词法域”是什么意思呢?这是指一个函数可以嵌套在另一个函数中,内部的函数可访问外部函数的变量。这项听起来平凡的特性将给语言带来极大的能力,因为它允许在Lua中应用各种函数式语言(functional-language)中强大的编程技术。

    a = {p = print}
    a.p('hello') -- hello
    
    print = math.sin
    a.p(print(1)) -- 0.8414709848079
    
    sin = a.p
    sin(10,20) -- 10 20
    

    Lua对函数式编程(functional programming)提供了良好的支持

    在Lua中有一个很容易混淆概念是,函数与所有其他值一样都是匿名的,即它们都没有名称。当讨论一个函数名时,实际上是在讨论一个持有某函数的变量。这与其他变量持有各种值的一个道理,可以以多种方式来操作这些变量。

    如果说函数是值的话,那是否可以说函数就是由一些表达式创建的呢?是的,事实上在Lua中最常见的是函数编写方式。

    function fn(x)
        return 2*x
    end
    
    -- 简化书写“语法糖”
    fn = function(x) return 2*x end
    

    一个函数定义实际就是一条语句,更准确地说是一条赋值语句,这条语句创建了一种类型为“函数”的值。

    可将表达式function(x) <body> end视为一种函数的构造式,类似table的构造式{}一样。将这种函数构造式的结果称为一个“匿名函数”,虽然一般情况下,会将函数赋予全局变量,即给予其一个名称。但在某些特殊情况下,仍会需要用到匿名函数。

    Lua即可以调用以自身Lua语言编写的函数,又可以调用以C语言编写的函数。

    table库提供了一个函数table.sort,它接收一个table并对其中的元素排序。向这种函数就必须支持各种各样可能的排序规则。sort函数并没有提供所有排序准则,而是提供了一个可选的参数,所谓“次序函数(order function)”。这个函数接收两个元素,并返回在有序情况下第一个元素是否已排在第二个元素之前。

    local tbl = {
        {name="ip1", ip="210.26,30,34"},
        {name="ip2", ip="210.26,30,33"},
        {name="ip3", ip="210.26,30,12"},
    }
    table.sort(tbl, function(x,y)
        return x.name > y.name
    end)
    for k,v in pairs(tbl) do
        print(v.name) --ip3 ip2 ip1
    end
    

    sort这样的函数,接收另一个函数作为实参,称其为“高阶函数(higher-order function)”。高阶函数是一种强大的编程机制,应用匿名函数来创建高阶函数所需的实参则可以带来更大的灵活性。但请记住,高阶函数并没有什么特权。Lua强调将函数视为“第一类值”,所以高阶函数只是一种基于该观点的应用体现而已。

    例如:关于导数的高阶函数

    function derivative(fn, delta)
        delta = delta or 1e-4
        return function(x)
            return (fn(x+delta)-fn(x))/delta
        end
    end
    
    fn = derivative(math.sin)
    print(math.cos(10), fn(10)) // -0.83907152907645    -0.83904432662041
    

    闭包函数

    当一个函数内部嵌套另一个函数定义时,内部的函数体可以访问外部函数的局部变量,这种特征称之为“词法界定”。词法界定加上第一类函数在编程语言中是一个功能强大的工具。

    若将函数写在另一个函数何内,那么这个位于内部的函数便可访问外部函数中的局部变量,这项特征称之为“词法域”。

    例如:根据每个学生的年级来对它们姓名进行由高到低的排序

    local userlist = {
        {username="mary", score=81},
        {username="shiva", score=92},
        {username="seth", score=65}
    }
    
    table.sort(userlist, function(x, y)
        return x.score > y.score
    end)
    
    for k,v in pairs(userlist) do
        print(k, v.username, v.score)
    end
    
    --[[
    1   shiva   92
    2   mary    81
    3   seth    65
    --]]
    

    创建函数完成操作

    function sort_by_grade(names, grades)
        table.sort(names, 
            function(a,b)
                return grades[a] > grades[b]
            end
        );
    end
    
    names = {"alice", "peter", "paul", "mary"}
    grades = {alice=10, peter=5, paul=9, mary=2}
    sort_by_grade(names,grades)
    
    for key,val in pairs(names) do
        print(key, val, grades[val])
    end
    

    有趣的是,传递给sort匿名函数可以访问参数grades,而grades是外部函数sort_by_grade的局部变量。在这个匿名函数内部,grades即不是全局变量也不是局部变量,将其称为一个非局部的变量(non-local variable)upvalues或“外部的局部变量(external local variable)”。为什么在Lua中允许这种访问呢?原因在于函数是“第一类值”。

    function count()
        local i = 0
        return function()
            i = i + 1
            return i
        end
    end
    
    cnt = count()
    print(cnt()) -- 1
    print(cnt()) -- 2
    print(cnt()) -- 3
    

    匿名函数访问了一个“非局部的变量ii变量用于保持一个计数器。Lua会以closure的概念来正确地处理这种情况。简单来说,一个closure就是一个函数加上该函数所需访问的所有“非局部的变量”。如果再次调用count(),那么它会创建一个新的局部变量i,从而也将得到一个新的closure

    技术上来讲,闭包指的是值而不是函数,函数仅仅是闭包的原型声明,尽管如此,在不会导致混淆的情况下,使用术语函数代指闭包。

    从技术上讲,Lua中只有closure,而不存在函数。因此,函数本身就是一种特殊的closure。不过只要不会引起混淆,仍将采用术语函数来指代closure

    在许多场合中closure都是一种很有价值的工具,例如:可作为sort类高阶函数的参数。closure对于创建其他函数也很有价值。这种机制使Lua可混合在函数式编程世界中久经考验的编程技术。另外closure对于回调函数也很有用。

    例如,假设有一个传统的GUI工具包可以创建按钮,每个按钮都有一个回调函数,每当用户按下按钮是GUI工具包都会调用这些回调函数。再假设,基于此要做一个十进制计算器,其中需要10个数字按钮,你会发现这些按钮之间的区别其实并不大,仅需在按下不同按钮时稍微不同的操作就可以了。

    -- 创建按钮
    function digitButton(digit)
        return Button{label=tostring(digit), action=function() add_to_display(digit) end}
    end
    

    假设Button是工具包中一个用于创建新按钮的函数,label是按钮的标签,action是回调closure,每当按钮按下时就会调用它。回调一般发生在digitButton函数执行完后,那时局部变量digit已经超出了作用范围,但closure仍可以访问到这个变量。

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

    通常当重新定义一个函数时,需要在新的实现中调用原来的那个函数。举例来说,假设要重新定义函数sin,使其参数能使用角度来替代原先的弧度。那么这个新函数就必须得转换它的实参,并调用原来的sin函数完成真正的计算。

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

    还有一种更彻底的做法

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

    将老版本的sin保存到一个私有变量,现在只有通过新版本的sin才能访问到它。

    可以使用同样的技术来创建一个安全的运行环境,即所谓的沙盒(sandbox)。当执行一些未受信任的代码时就需要一个安全的运行环境,例如在服务器中执行那些从Internet上接收到的代码。

    举例来说,如果要限制一个程序访问文件的话,只需要使用closure来重新定义函数io.open()即可。

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

    经过重新定义后,一个函数就只能通过受限版本来调用原来那个未受限的open()函数了。将原来不安全的版本保存到closure的一个私有变量中,从而使得外部再也无法直接访问到原来的版本了。

    通过这种技术,可以再Lua的语言层面上就构建出一个安全的运行环境,且不失建议性和灵活性。相对于提供一套大而全的解决方案,Lua提供的则是一套“元机制(meta-mechanism)”,因此可以根据特定的安全需要来创建一个安全的运行环境。

    非全局的函数

    由于函数是一种“第一类值”,因此一个显而易见的推论是,函数不仅可以存储在全局变量中,也可以存储在table的字段中和局部变量中

    Lua中大部分库采用将函数存储在table字段中这种机制。若要在Lua中创建这种函数,只需将常规的函数语法和table语法结合即可。

    Lib = {}
    Lib.foo = function(x,y)
        return x+y
    end
    Lib.bar = function(x,y)
        return x-y
    end
    

    当然,也可使用构造式

    Lib = {
        foo=function(x,y) return x+y end,
        bar=function(x,y) return x-y end
    }
    

    只要将一个函数存储到一个局部变量中,即得到一个局部函数(local function),也就是说该函数只能在某个特定的作用域中使用。

    对于程序包(package)而言,这种函数定义是非常有用的,因为Lua是将每个特定程序块(chunk)作为一个函数来处理的,所以在一个程序块中声明的函数就是局部函数,这些局部函数只在该程序块中可见。词法域确保了程序包中的其他函数可以使用这些局部函数。

    local fn = function(arg)
        -- function body
    end
    
    local func = function(arg)
        fn()
    end
    

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

    local function fn(arg)
        -- function body
    end
    

    在定义递归的局部函数中,有一个特别之处需要注意。

    local face = function(n)
        if n==0 then
            return 1
        else
            -- 错误的递归调用
            -- 当Lua编译到函数体中调用fact(n-1)的地方时,由于局部的fact尚未定义完毕。
            -- 因此这句表达式其实调用了一个全局的fact,而非此函数本身。
            return n*face(n-1)
        end
    end
    
    --[[结局方案:可以先定义一个局部变量,然后再定义函数本身。--]]
    local fact
    fact = function(n)
        if n==0 then return 1
        else return n*fact(n-1)
        end
    end
    --[[
    现在函数中的fact调用就表示了局部变量,即使在函数定义时,这个局部变量的值尚未完成定义,但之后在函数执行时,fact则肯定拥有了正确的值。
    --]]
    

    当Lua展开局部函数定义的“语法糖”时,并不是使用基础函数定义语法。而是对于局部函数定义:

    local function fn(<args>) <function body> end
    

    Lua将其展开为

    local fn
    
    fn = function(<args>) <function body> end
    

    因此,使用此种语法定义递归函数不会产生错误:

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

    当然,这个技巧对于间接递归的函数而言是无效的。对于间接递归的情况下,必须使用一个明确的向前声明(Forward Declaration):

    local fn,func -- 向前声明
    
    function func()
      fn()
    end
    
    function fn()
      func()
    end
    
    --[[
    注意,别把第二个函数定义为"local function fn"。
    如果那样的话,Lua会创建一个全新的局部变量fn。
    而将原来声明的fn(func函数中所引用的那个)置于未定义状态。
    --]]
    

    尾调用

    Lua函数有一个有趣的特征,那就是Lua支持“尾调用消除(tail-call elimination)”。

    所谓“尾调用(tail call)”就是一种类似于goto()的函数调用,当一个函数调用是另一个函数的最后一个动作时,该调用才算是一条“尾调用”。

    function fn(x)
      return func(x)
    end
    
    --[[
    当fn()函数调用完func()函数之后就再无其他事情可做了
    因此在这种情况下,程序就不需要返回那个“尾调用”所在的函数了。
    所以在“尾调用”之后,程序也不需要保存任何关于该函数的栈(stack)信息。
    当func()函数返回时,执行控制权可以直接返回给调用fn()函数的那个点上。
    有些语言实现(例如Lua解释器)可以得益于这个特点,使得在进行尾调用时不耗费任何栈空间。
    将这种实现称之为支持“尾调用消除”。
    --]]
    

    由于“尾调用”不会耗费栈空间,所以一个程序可以拥有无数嵌套的“尾调用”。

    function fn(n)
      if n>0 then 
        return fn(n-1)
      end
    end
    
    --[[
    在调用fn()函数时,传入任何数字n作为参数都不会造成栈溢出。
    --]]
    

    有一点需要注意的是,当想要受益于“尾调用消除”时,务必要确定当前的调用时一条“尾调用”。判断的准则是“一个函数在调用完另一个函数之后,是否就无其他事情需要做了”。有些看似“尾调用”的代码,其实都违背了这条准则。

    function fn(x)
      func(x)
    end
    
    --[[
    当调用完func()函数后,fn()函数并不能立即返回,它还需丢弃func()函数返回的临时结果。
    --]]
    

    Lua中,只有return <func>(<args>)这样的调用形式才算是一条“尾调用”。

    return fn(x)+1 -- 必须做一次加法
    
    return x or fn(x) -- 必须调整为一个返回值
    
    return (g(x)) -- 必须调整为一个返回值
    

    Lua在调用前对<func>及其参数求值,所以它们可以是任意复杂的表达式。

    return x[i].fn(x[j]+a*b, i+j)
    

    一条“尾调用”就好比是一条goto语句。因此,在Lua中“尾调用”的一大应用就是编写“状态机(state machine)”。这种程序通常以一个函数来表示一个状态,改变状态就是goto(或调用)到另一个特定的函数和。

    例如:一个简单的迷宫游戏中,一个迷宫有几间房间,每间房中最多有东南西北4扇门。用户在每一步异动中都需要输入一个移动的方向。如果在某个方向上有门,那么用户可以进入相应的房间。不然,程序就打印一条警告。游戏的目标就是让用户从最初的房间走到最终的房间。

    这个游戏就是典型的状态机,其中当前房间就是一个状态。可以将迷宫的每间房实现为一个函数,并使用“尾调用”来实现从一间房移动到另一件。

    function room1()
        local move = io.read()
        if move=='south' then 
            return room3()
        elseif move=="east" then 
            return room2()
        else 
            print("invalid move")
            return room1()
        end
    end
    
    function room2()
        local move = io.read()
        if move=='south' then
            return room4()
        elseif move=='west' then
            return room1()
        else
            print('invalid move')
            return room2()
        end
    end
    
    function room3()
        local move=io.read()
        if move=='north' then
            return room1()
        elseif move=='ease' then
            return room4()
        else
            print("invalid move")
            return room3()
        end
    end
    
    function room4()
        print("congratulations")
    end
    -- 调用初始房间来开始游戏
    room1()
    
    --[[
    若没有“尾调用消除”,每次用户的异动都会创建一个新的栈层(stack level)
    异动若干步后就有可能会导致栈溢出
    “尾调用消除”则对用户异动的次数没有任何限制
    因为每次异动实际上都只是完成一条goto语句到另一个函数,而非传统的函数调用。
    --]]
    

    对于简单的游戏而言,或许觉得将程序设计为数据驱动的会更好一些,其中将房间和异动记录在table中。不过,如果游戏中每间房间都有各自特殊的情况的话,采用这种状态机的设计则更为合适。

    Lua迭代器与闭包

    迭代器是一种可以遍历(iterate over)集合中所有元素的机制。在Lua中通常将迭代器表示为函数,每次调用函数即返回集合中的“下一个”元素。

    每个迭代器都需要在每次成功调用之间保持一些状态,这样才能知道它所在的位置及如何步进到下一个位置。闭包(closure)对于这类任务提供极佳的支持,一个闭包就是一种可以访问其外部嵌套环境中的局部变量的函数。

    对于闭包而言,这些变量就可用于在成功调用之间保持状态值,从而使闭包可以记住它在一次遍历中所在的位置。

    当然,为了创建一个新的闭包,还必须创建它的“非局部变量(non-local variable)”。因此,一个闭包结构通常涉及到两个函数:闭包本身和一个用于创建该闭包的工厂函数。

    -- 为列表编写简单的迭代器,与ipaires不同的是该迭代器并不是返回每个元素的索引,而是返回元素的值。
    -- values是一个工厂,每当调用这个工厂时,就创建一个新的闭包(迭代器本身)。这个闭包将它的状态保存在其外部变量tbl和i中。
    function values(tbl)
        local i=0
        return function()
            i = i+1
            return tbl[i]
        end
    end
    
    -- 每当调用这个迭代器时,它就从列表tbl中返回下一个值。
    -- 直到最后一个元素返回后,迭代器就会返回nil,以此表示迭代器的结束。
    tbl={1,2,3,4}
    iter = values(tbl) -- 创建迭代器
    while true do
        local el = iter() -- 调用迭代器
        if el==nil then
            break
        end
        print(el)
    end
    

    然而,使用泛型for则更为简单,你会发现泛型for正是为这种迭代而设计的。

    function values(tbl)
        local i=0
        return function()
            i = i+1
            return tbl[i]
        end
    end
    
    -- 泛型for为一次迭代循环做了所有的薄记工作。
    -- 它在内部保存了迭代器函数,因此不再需要iter变量。
    -- 它在每次新迭代时调用迭代器,并在迭代器返回nil时结束循环。
    tbl={1,2,3,4}
    for el in values(tbl) do
        print(el)
    end
    

    需求:遍历当前输入文件中所有单词的迭代器
    为完成这样的遍历,需要保持两个值:当前行的内容、当前行所处的位置。有了这些信息就可以不断产生下一个单词。迭代器函数的主要部分使用 string.find在当前行中以当前位置作为起始来所搜索一个单词。使用模式%w+来描述一个单词,它用于匹配一个或多个的文字/数字字符。如果string.find找到了一个单词,迭代器就会将当前位置更新为该单词之后的第一个字符,并返回该单词。否认则,它就读取新的一行并反复这个搜索过程。若没有剩余的行,则返回nil表示迭代的结束。

    function allwords()
        local line = io.read()
        local pos = 1
    
        return function()
            while line do
                local s,e = string.find(line, "%w+", pos)
                if s then
                    pos = e+1
                    return string.sub(line,s,e)
                else
                    line = io.read()
                    pos = 1
                end
            end
            return nil
        end
    end
    
    for word in allwords() do
        print(word)
    end
    

    相关文章

      网友评论

        本文标题:Lua函数

        本文链接:https://www.haomeiwen.com/subject/qkrdqqtx.html