函数

  1. 多重返回值
  2. 变长参数
  3. 具名实参
  4. 深入函数
  5. closure(闭合函数)
  6. 非全局的函数
  7. 正确的尾调用

  

  一个函数若只有一个参数,并且此参数是一个字面字符串或table构造式,那么圆括号便是可有可无的。

  1. -- 例如:
  2. print "Hello World" <--> print("Hello World")
  3. dofile 'a.lua' <--> dofile('a.lua')
  4. print [[a multi-line <--> print([[a multi-line
  5. message]] message]]
  6. f{x=10, y=20} <--> f({x=10, y=20})
  7. type{} <--> type({})

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

  1. o.foo(o, x) -- 另一种写法是 o::foo(x)

  冒号操作符使调用o.foo时将o隐含地作为函数第一个参数。  

  一个Lua程序既可以使用以Lua编写的函数,又可以调用以C语言编写的函数,调用时没有任何区别。

    调用函数时提供的实参数量可以与形參数量不同。Lua会自动调整实参的数量,以匹配参数表的要求。

  1. -- 假设一个函数如下:
  2. function f(a, b) return a or b end
  3. -- 在以下几种调用中,实参与形參的对应关系为:
  4. -- 调用 形參
  5. f(3) a=3, b=nil
  6. f(3, 4) a=3, b=4
  7. f(3, 4, 5) a=3, b=4 (5被丢弃了)
  1. -- 默认实参的应用
  2. function incCount(n)
  3. n = n or 1
  4. count = count + n
  5. end

  

  • 多重返回值

  Lua允许函数返回多个结果。例如,用于字符串中定位一个模式的函数string.find。该函数若在字符串中找到了指定的模式,将返回匹配的起始字符和结尾字符的索引。

  1. startIndex, endIndex = string.find("hello Lua users", "Lua")
  2. print(startIndex, endIndex) --> 7 9

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

  1. -- 查找数组中的最大元素,并返回该元素的位置:
  2. function maximum(a)
  3. local index = 1 -- 最大值的索引
  4. local max = a[index] -- 最大值
  5. for i,val in ipairs(a) do
  6. if val > max then
  7. max = val; index = i
  8. end
  9. end
  10. return max, index
  11. end
  12. print(maximum{8, 10, 23, 12, 5}) --> 23 3

  Lua会调整一个函数的返回值数量以适应不同的调用情况。若将函数调用作为一条单独语句时,Lua会丢弃函数的所有返回值。若将函数作为表达式的一部分来调用时,Lua只保留函数的第一个返回值。只有当一个函数调用是一系列表达式中的最后一个元素(或仅有一个元素)时,才能获得它的所有返回值。    这里所谓的“一系列表达式”在Lua中表现为4种情况:多重赋值、函数调用时传入的实参列表、table的构造式和return语句。

  1. function foo0() end -- 无返回值
  2. function foo1() return "a" end -- 返回1个结果
  3. function foo2() return "a", "b" end -- 返回2个结果
  1. x,y = foo2() -- x="a", y="b"
  2. x = foo2() -- x="a", "b"被丢弃
  3. x,y,z = 10, foo2() -- x=10, y="a", z="b"
  4. x,y = foo0() -- x=nil, y=nil
  5. x,y = foo1() -- x="a", y=nil
  6. x,y,z = foo2() -- x="a", y="b", z=nil
  7. -- 函数调用不是表达式最后一个元素,将只产生一个值:
  8. x,y = foo2(), 20 -- x="a", y=20
  9. x,y = foo0(), 20, 30 -- x=nil, y=20, 30被丢弃
  10. -- 函数调用作为另一个函数调用的最后一个(或仅有的)实参时,第一个函数的所有返回值都将作为实参传入第二个函数。如print
  11. print(foo0()) -->
  12. print(foo1()) --> a
  13. print(foo2()) --> a b
  14. print(foo2(), 1) --> a 1
  15. print(foo2() .. "x") --> ax

  table构造式可以完整地接收一个函数调用的所有结果:

  1. t = {foo0()} -- t = {} (一个空的table)
  2. t = {foo1()} -- t = {"a"}
  3. t = {foo2()} -- t = {"a", "b"}

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

  1. t = {foo0(), foo2(), 4} -- t[1] = nil, t[2] = "a", t[3] = 4

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

  1. function foo(i)
  2. if i==0 then return foo0();
  3. elseif i==1 then return foo1();
  4. elseif i==2 then return foo2()
  5. end
  6. end
  7. print(foo(1)) -- a
  8. print(foo(2)) -- a b
  9. print(foo(0)) -- (无返回值)
  10. print(foo(3)) -- (无返回值)

  也可以将一个函数调用放入一对圆括号中,从而迫使它只返回一个结果:

  1. print((foo0())) -- nil
  2. print((foo1())) -- a
  3. print((foo2())) -- a

  请注意return语句后面的内容是不需要圆括号的。例如return (f(x)),将只返回一个值,而无关乎f返回了几个值。

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

  1. print(unpack{10, 20, 30}) --> 10 20 30
  2. a,b = unpack{10, 20, 30} --> a=10, b=20, 30被丢弃
Lua5.1及之前的版本中,unpack作为全局函数使用,可以直接

  1. unpack(arg)


而5.2之后,unpack被移到了table下面,于是直接unpack就会导致报错,新的调用应该为:

  1. table.unpack(arg)


同时修改的还有另外一个:arg以前 可以在函数内直接使用arg来处理,但是5.1之后,就需要自己手动变换成arg

  1. local arg = {…}
    print(arg[1])


提供一种兼容的方法

  1. function test(…)
    if arg ~= nil then
    arg = {…}
    end
    if unpack != nil then 5.1及之前的版本
    print(unpack(arg))
    else 之后的版本
    local arg = {…}
    print(table.unpack(arg))
    end
    end



  1. f = string.find
    a = {"hello", "ll"}


  f(unpack(a))将返回3和4,这与直接调用string.find("hello", "ll") 所返回的结果一摸一样。  虽然这个预定义函数unpack是用C语言直接编写的,但是仍可以在Lua中通过递归实现一样效果:

  1. function unpack(t, i)
    i = i or 1
    if t[i] then
    return t[i], unpack(t, i+1)
    end
    end


  

  • 变长参数

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

  1. -- 这个函数返回了所有参数的总和:
  2. function add( ... )
  3. local s = 0
  4. for i, v in ipairs( ... ) do -- 表达式{...}表示一个由所有变长参数构成的数组。
  5. s = s + v
  6. end
  7. return s
  8. end
  9. print(add(3, 4, 10, 25, 12)) --> 54

  参数中的3个点()表示该函数可接受不同数量的实参。    Lua提供了专门用于格式化文本(string.format)和输出文本(io.write)的函数。

  1. function fwrite(fmt, ...)
  2. return io.write(string.format(fmt, ...))

  注意在3个点前有一个固定参数fmt。具有变长参数的函数同样也可以拥有任意数量的固定参数,但固定参数必须放在变长参数之前。    变长参数中可能会包含一些故意传入的nil,那么此时就需要用函数select来访问变长参数了。调用select时,必须传入一个固定实参selector(选择开关)和一系列变长参数。如果selector为数字n,那么select返回它的第n个可变实参;否则,selector只能为字符串“#”,这样select会返回变长参数的总数。

  1. for i=1, select('#', ...) do
  2. local arg = select(i, ...) -- 得到第i个参数
  3. <循环体>
  4. end

  特别需要指出的是,select('#', …)会返回所有变长参数的总数,其中包括nil

  

  • 具名实参

  1. -- 无效的演示代码
  2. rename(old="temp.lua", new="temp1.lua")

  Lua并不直接支持这种语法,但可以通过一种细微的改变来获得相同的效果。主要是将所有实参组织到一个table中,并将这个table作为唯一的实参传给函数。另外,还需要用到一种Lua中特殊的函数调用语法,就是当实参只有一个table构造式时,函数调用中的圆括号是可有可无的:

  1. rename{old="temp.lua", new="temp1.lua"}

  另一方面,将rename改为只接受一个参数,并从这个参数中获取实际的参数:

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

  若一个函数拥有大量的参数,而其中大部分参数是可选的话,这种参数传递风格会特别有用。例如在一个GUI库中,一个用于创建新窗口的函数可能会具有许多的参数,而其中大部分都是可选的,那么最好使用具名实参:

  1. w = Window{ x=0, y=0, width=300, height=200,
  2. title="Lua", background="blue", border=true
  3. }

  Window函数可以根据要求检查一些必填参数,或者为某些参数添加默认值。假设“_Window”才是真正用于创建新窗口的函数,它要求所有参数以正确的次序传入,那么Window函数可以这么写:

  1. function Window(options)
  2. -- 检查必要的参数
  3. if type(options.title) ~= "string" then
  4. error("no tile")
  5. elseif type(options.width) ~= "number" then
  6. error("no width")
  7. end
  8. -- 其他参数都是可选的
  9. _Window(options.title,
  10. options.x or 0, -- 默认值
  11. options.y or 0, -- 默认值
  12. options.width, options.height,
  13. options.backgournd or "white", -- 默认值
  14. options.border -- 默认值为false(nil)
  15. )
  16. end

  

  • 深入函数

  函数可以存储到变量中(无论全局变量还是局部变量)或table中,可以作为实参传递给其他函数,还可以作为其他函数的返回值。

  一个函数可以嵌套在另一个函数中,内部的函数可以访问外部函数中的变量。

  函数与其他值一样都是匿名的。当讨论一个函数名时(例如print),实际上是在讨论一个持有某函数的变量。

  1. a = {p = print}
  2. a.p("Hello World") --> Hello World
  3. print = math.sin --> 'print'现在引用了正弦函数
  4. a.p(print(1)) --> 0.841470
  5. sin = a.p --> 'sin'现在引用了print函数
  6. sin(10, 20) --> 10 20
  1. -- Lua中最常见的是函数编写方式,诸如:
  2. function foo(x) return 2*x end

  只是一种所谓的“语法糖”而已。也就是说,这只是以下代码的一种简化书写形式:

  1. foo = function(x) return 2*x end

  因此,一个函数定义实际就是一条语句(更准确地说是一条赋值语句),这条语句创建了一种类型为“函数”的值,并将这个值赋予一个变量。  可以将表达式“function(x)<body>end”视为一种函数的构造式,就像table的构造式{}一样。  将这种函数构造式的结果称为一个“匿名函数”。虽然一般情况下,会将函数赋予全局变量,即给予其一个名称。但在某些特殊情况下,仍会需要用到匿名函数。

    table库提供了一个函数table.sort,它接受一个table并对其中的元素排序。像这种函数就必须支持各种各样可能的排序准则,例如升序还是降序、按数字顺序还是按字符顺序或者按tablekey的顺序等。sort函数并没有提供所有这些排序准则,而是提供了一个可选的参数,所谓“次序函数”。这个函数接受两个元素,并返回在有序情况下第一个元素是否应排在第二个元素之前。举例来说,假设有一个table内容如下:

  假设有一个table内容如下:

  1. network = {
  2. {name = "grauna", IP = "210.26.30.34"},
  3. {name = "arraial", IP = "210.26.30.23"},
  4. {name = "lua", IP = "210.26.23.12"},
  5. {name = "derain", IP = "210.26.23.20"}
  6. }

  如果想以name字段、按反向的字符顺序来对这个table排序的话,只需这么写:

  1. table.sort(network, function(a,b) return (a.name > b.name) end)
  1. for i, v in ipairs( network ) do
  2. print( v["name"] )
  3. end

  可见匿名函数在这条语句中就显示出了极好的便捷性。

  像sort这样的函数,接受另一个函数作为实参的,称其是一个“高阶函数”。高阶函数是一种强大的编程机制,应用匿名函数来创建高阶函数所需的实参则可以带来更大的灵活性。

  

  • closure(闭合函数)

  若将一个函数写在另一个函数之内,那么这个位于内部的函数便可以访问外部函数中的局部变量,这项特征称之为“词法域”。    先来看一个简单的例子。假设有一个学生姓名的列表和一个对应于每个姓名的年级列表,需要根据每个学生的年级来对他们的姓名进行排序(由高到低)。可以这么做:

  1. name = {"Peter", "Paul", "Mary"}
  2. grades = {Mary = 10, Paul = 7, Peter = 8}
  3. table.sort(names, function(n1, n2)
  4. return grades[n1] > grades[n2] -- 比较年级
  5. end)

  现在假设要单独创建一个函数来做这项工作:

  1. function sortbygrade(names, grades)
  2. table.sort(names, function(n1, n2) return grades[n1] > grades[n2] end)
  3. end

  在上例中有一点很有趣,传递给sort的匿名函数可以访问参数grades,而grades是外部函数sortbygrade的局部变量。在这个匿名函数内部,grades既不是全局变量也不是局部变量,将其称为一个“非局部的变量”。

    为什么在Lua中允许这种访问呢?原因在于函数是“第一类值”。考虑以下代码:

  1. function newCounter()
  2. local i = 0
  3. return function() i = i + 1 return i; end
  4. end
  5. c1 = newCounter()
  6. print(c1()) --> 1
  7. print(c2()) --> 2

  在这段代码中,匿名函数访问了一个“非局部的变量”i,该变量用于保持一个计数器。  初看上去,由于创建变量i的函数(newCounter)已经返回,所以之后每次调用匿名函数时,i都应是已超出了作用范围的。但其实不然,Lua会以closure的概念来正确地处理这种情况。简单地讲,一个closure就是一个函数加上该函数所需访问的所有“非局部的变量”。如果再次调用newCounter,那么它会创建一个新的局部变量i,从而也将得到一个新的closure

  1. c2 = newCounter()
  2. print(c2()) --> 1
  3. print(c1()) --> 3
  4. print(c2()) --> 2

  因此c1c2是同一个函数所创建的两个不同的closure,它们各自拥有局部变量i的独立实例。

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

    closure在另一种情况中也非常有用。例如在Lua中函数是存储在普通变量中的,因此可以轻易地重新定义某些函数,甚至是重新定义那些预定义的函数。这也正是Lua相当灵活的原因之一。通常当重新定义一个函数的时候,需要在新的实现中调用原来的那个函数。举例来说,假设要重新定义函数sin,使其参数能使用角度来代替原来的弧度。那么这个新函数就必须得转换它的实参,并调用原来的sin函数完成真正的计算。这段代码可能是这样的:

  1. oldSin = math.sin
  2. math.sin = function(x)
  3. return oldSin(x*math.pi/180)
  4. end

还有一种更彻底的做法是这样的:

  1. do
  2. local oldSin = math.sin
  3. local k = math.pi/180
  4. math.sin=function(x)
  5. return oldSin(x*k)
  6. end
  7. end

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

    可以使用同样的技术来创建一个安全的运行环境,即所谓的“沙盒”。当执行一些未受信任的代码时就需要一个安全的运行环境,例如在服务器中执行那些从Internet上接收到的代码。举例来说,如果要限制一个程序访问文件的话,只需使用closure来重新定义函数io.open就可以了。

  1. do
  2. local oldOpen = io.open
  3. local access_OK = function(filename, mode)
  4. <检查访问权限>
  5. end
  6. io.open = function(filename, mode)
  7. if access_OK(filename, mode) then
  8. return oldOpen(filename, mode)
  9. else
  10. return nil, "access denied"
  11. end
  12. end
  13. end

  这个示例的精彩之处在于,经过重新定义后,一个程序就只能通过新的受限版本来调用原来那个未受限的open函数了。示例将原来不安全的版本保存到了closure的一个私有变量中,从而使得外部再也无法直接访问到原来的版本了。通过这种技术,可以在Lua的语言层面上就构建出一个安全的运行环境,且不失简易性和灵活性。相对于提供一套大而全的解决方案,Lua提供的则是一套“元机制”,因此可以根据特定的安全需要来创建一个安全的运行环境。

  

  • 非全局的函数

  由于函数是一种“第一类值”,因此一个显而易见的推论就是,函数不仅可以存储在全局变量中,还可以存储在table的字段中和局部变量中。前面讲到了几个将函数存储在table字段中的示例,大部分Lua库也采用了这种机制(例如io.readmath.sin)。若要在Lua中创建这种函数,只需将常规的函数语法与table语法结合起来使用即可:

  1. Lib = {}
  2. Lib.foo = function(x, y) return x + y end
  3. Lib.goo = function(x, y) return x - y end

当然,还可以使用构造式:

  1. Lib = {
  2. foo = function(x, y) return x + y end,
  3. goo = function(x, y) return x - y end
  4. }

  除了这些之外,Lua还提供了另一种语法来定义这类函数:

  1. Lib = {}
  2. function Lib.foo(x, y) return x + y end
  3. function Lib.goo(x, y) return x - y end

  只要将一个函数存储到一个局部变量中,即得到了一个“局部函数”,也就是说该函数只能在某个特定的作用域中使用。对于“程序包”而言,这种函数定义是非常有用的。因为Lua是将每个程序块作为一个函数来处理的,所以在一个程序块中声明的函数就是局部函数,这些局部函数只在该程序块中可见。词法域确保了程序包中的其他函数可以使用这些局部函数:

  1. local f = function(<参数>)
  2. <函数体>
  3. end
  4. local g = function(<参数>)
  5. <一些代码>
  6. f()
  7. <一些代码>
  8. end

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

  1. local function f(<参数>)
  2. <函数体>
  3. end

  

  在定义递归的局部函数时,还有一个特别之处需要注意。像下面这种采用了基本函数定义语法的代码多数是错误的:

  1. local fact = function(n)
  2. if n == 0 then return 1
  3. else return n * fact(n-1) -- 错误❌
  4. end
  5. end

  当Lua编译到函数体中调用fact(n-1)的地方时,由于局部的fact尚未定义完毕,因此这句表达式其实是调用了一个全局的fact,而非此函数自身。为了解决这个问题,可以先定义一个局部变量,然后再定义函数本身:

  1. local fact
  2. fact = function(n)
  3. if n == 0 then return 1
  4. else return n * fact(n-1)
  5. end
  6. end

  现在函数中的fact调用就表示了局部变量。即使在函数定义时,这个局部变量的值尚未完成定义,但之后在函数执行时,fact则肯定已经拥有了正确的值。

  

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

  1. local funciton foo(<参数>) <函数体> end

  Lua将其展开为:

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

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

  1. local function fact(n)
  2. if n == 0 then return 1
  3. else return n * fact(n-1)
  4. end
  5. end

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

  1. local f, g -- 前向声明
  2. function g()
  3. <一些代码> f() <一些代码>
  4. end
  5. function f()
  6. <一些代码> g() <一些代码>
  7. end
  注意⚠️,别把第二个函数定义写为“local function f”。如果那样的话,Lua会创建一个全新的局部变量f,而将原来声明的f(函数g中所引用的那个)置于未定义的状态。

  

  • 正确的尾调用

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

  

  举例来说,以下代码中对g的调用就是一条“尾调用”:

  1. function f(x) return g(x) end

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

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

  

  举例来说,在调用以下函数时,传入任何数字作为参数都不会造成栈溢出:

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

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

    举例来说,在下面的代码中,对g的调用就不是一条“尾调用”:

  1. function f(x) g(x) end

  这个示例的问题在于,当调用完g后,f并不能立即返回,它还需要丢弃g返回的临时结果。类似地,以下所有调用也都不符合上述准则:

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

    在Lua中,只有“return <func>(<args>)”这样的调用形式才算是一条“尾调用”。Lua会在调用前对<func>及其参数求值,所以它们可以是任意复杂的表达式。

  举例来说,下面的调用就是一条“尾调用”:

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

  

  在之前提到了,一条“尾调用”就好比是一条goto语句。因此,在Lua中“尾调用”的一大应用就是编写“状态机”。这种程序通常以一个函数来表示一个的状态,改变状态就是goto(或调用)到另一个特定的函数。举一个简单的迷宫游戏的例子来说明这个问题。

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

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

  1. function room1()
  2. local move = io.read()
  3. if move == "south" then return room3()
  4. elseif move == "east" then return room2()
  5. else
  6. print("invalid move")
  7. return room1() -- stay in the same room
  8. end
  9. end
  10. function room2()
  11. local move = io.read()
  12. if move == "south" then return room4()
  13. elseif move == "west" then return room1()
  14. else
  15. print("invalid move")
  16. return room2() -- stay in the same room
  17. end
  18. end
  19. function room3()
  20. local move = io.read()
  21. if move == "north" then return room1()
  22. elseif move == "east" then return room4()
  23. else
  24. print("invalid move")
  25. return room3() -- stay in the same room
  26. end
  27. end
  28. function room4()
  29. print("congratulations!")
  30. end

  通过调用初始房间来开始这个游戏:

  1. room1()

  若没有“尾调用消除”的话,每次用户的移动都会创建一个新的栈层,移动若干步之后就有可能会导致栈溢出。而“尾调用消除”则对用户移动的次数没有任何限制。这是因为每次移动实际上都只是完成一条goto语句到另一个函数,而非传统的函数调用。

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

?