谈一谈Lua中函数的定义方式以及有关特性

1,174 阅读8分钟

前言

在Lua语言中,函数是对语句和表达式进行抽象的主要方式。函数既可以用于完成某种特定任务或子例程,也可以只是进行一些计算然后返回计算结果。前一种情况下,我们将一句函数调用视为一条语句;而在后一种情况下,我们则将函数调用视为表达式。

语句:print(8*9,9/8)
表达式:a = math.sin(3) + math.cos(10)

注意: 无论哪种情况,函数调用时都需要使用一对圆括号把参数列表括起来,即使被调用的函数不需要参数,也需要一对空括号()。对于这个规则,唯一例外的就是,当函数只有一个参数且该参数是字符串常量或表构造器时,括号是可选的

print "Hello World"             <-->  print("Hello World")
dofile 'a.lua'                  <-->  dofile('a.lua')
print [[a multi-line message]]  <-->  print([[a multi-line message]])
f{x=10,y=20}                    <-->  f({x=10,y=20})
type{}                          <-->  type({})

函数定义的常见语法:

-- 对序列'a'中的元素求和
function add(a)
    local sum = 0
    for i = 1, #a do
        sum = sum + a[i]
    end
    return sum
end

在这种语法中,一个函数定义具有一个函数名,一个参数组成的列表和由一组语句组成的函数体。参数的行为与局部变量的行为完全一致,相当于一个用函数调用时传入的值进行初始化的局部变量。

调用函数时使用的参数个数可以与定义函数时使用的参数个数不一致。Lua语言会通过抛弃多余参数和将不足的参数设为nil的方式来调整参数的个数。

例如: function f(a, b) print(a, b) end 其行为如下:

f()         -->  nil  nil
f(3)        -->   3   nil
f(3, 4)     -->   3    4
f(3, 4, 5)  -->   3    4   (5被丢弃)

多返回值

lua语言中一种与众不同但又非常有用的特性是允许一个函数返回多个结果,例如函数string.find(),该函数会返回所匹配模式在字符串中起始字符串和结尾字符串的索引。Lua语言编写的函数同样可以返回多个结果,只需要在return关键字后面列出所有要返回的值即可。

-- 一个用于查找序列中最大元素的函数可以同时返回最大值以及改元素的位置
function maximun(a)
    local mi = 1   --最大值的索引
    local m = a[mi]  --最大值
    for i = 1, #a do
        if a[i] > m then
            mi = i;
            m = a[i];
        end
    end
    return m, mi   --返回最大值及其索引
end

  1. Lua语言会根据函数的被调用情况调整返回值的数量。
  • 当函数被作为一条单独语句调用时,其所有返回值都会被丢弃。
  • 当函数被作为表达式调用时,将只保留函数的第一个返回值。
  • 当函数调用是一系列表达式中的最后一个表达式时,其所有的返回值才能被获取到。
  1. 所谓的一系列表达式,在lua中表现为4种情况:
  • 多重赋值
  • 函数调用时传入的实参列表
  • 表构造器
  • return语句 为了分别展示这几种情况,接下来举几个例子:
function foo0()  end                 --不返回结果
function foo1()  return "a" end      --返回1个结果
function foo2()  return "a","b" end  --返回2个结果

在多重赋值中,如果一个函数调用是一系列表达式中的最后(或者是唯一)一个表达式,则该函数调用则尽可能多的返回值以匹配赋值变量:

x, y = foo2()         -- x = "a",y="b"
x = foo2()            -- x = "a","b"被丢弃
x, y, z = 10,foo2()   -- x = 10, y = "a", z = "b"

在多重赋值中,如果一个函数没有返回值或者返回值个数不够,那么lua语言会用nil来补充缺失的值:

x, y = foo0()     -- x = nil, y = nil
x, y = foo1()     --x = "a", y = nil
x, y, z = foo2()  -- x = "a", y = "b", z = nil

注意:只有当函数调用是一一系列表达式中的最后一个(或者是唯一)一个表达式时才能返回多值结果,否则只能返回一个结果。

x, y = foo2(), 20      -- x = "a", y =20 ('b'被丢弃)
x, y = foo0(), 20, 30  -- x = nil, y =20 (30被丢弃)

最后,形如return f()的语句会返回f返回的所有结果:

function foo(i)
    if i == 0 then return foo0()
    elseif i == 1 then reutrn foo1()
    elseif i == 2 then return foo2()
    end
end

print(foo(1))   -->  a
print(foo(2))   -->  a  b
print(foo(0))   --> (无结果)
print(foo(3))   --> (无结果)

--将函数调用用一对圆括号括起来可以强制其只返回一个结果:
print((foo0()))   --> nil
print((foo1()))   --> a
print((foo2()))   --> a

应该意识到,return语句后面的内容是不需要加括号的


可变长参数函数

Lua语言中的函数可以是可变长参数,即可以支持数量可变的参数。参数列表中的三个点(...)表示该函数的参数是可变长的,当这个函数被调用时,lua内部会把它的所有参数搜集起来,我们把这些被搜集起来的参数称为函数的额外参数。当函数要访问这些参数时仍需要用到这三个点,但不同的是此时这三个点是作为一个表达式来使用的。

function add(...)
    local s = 0
    for _, v in ipairs{...} do
        s = s + v
    end
    return s
end
print(add(3, 4, 10, 25, 12))  -->  54

我们将三个点组成的表达式称为可变长参数表达式。其行为类似于一个具有多个返回值的函数,返回的是当前函数的所有可变长参数。实际上可以通过边长参数来模拟lua语言中普通的参数传递机制,例如:

function foo(a, b, c)
--可以写成:
function foo(...)
    local a, b, c = ...

当可变长参数中包含无效的nil,那么{...}获得的表可能不再是一个有效的序列,对于这种情况,可以使用lua提供的函数table.pack,该函数像表达式{...}一样保存所有参数,然后将其放在一个表中返回,但是这个表还有一个保存了参数个数的额外字段"n"

--使用函数table.pack来检测参数中是否有nil
function nonils(...)
    local arg = table.pack(...)
    for i = 1, arg.n do
        if arg[i] == nil then return false end
    end
    return true
end

print(nonils(2, 3, nil))  --> false
print(nonils(2, 3))       --> true
print(nonils())           --> true
print(nonils(nil))        --> false

另一种遍历函数的可变长参数的方法是使用函数select(n,...),函数select总是具有一个固定的参数selector,以及数量可变的参数,如果selector是数值n,那么函数select则返回第n个参数后的所有参数(包括n);否则,selector应该是字符串"#",此时返回额外参数的总数。

print(select(1, "a", "b", "c"))  --> a b c
print(select(2, "a", "b", "c"))  --> b c
print(select(3, "a", "b", "c"))  --> c
print(select(#, "a", "b", "c"))  --> 3

函数table.unpack

顾名思义,函数table.unpack与函数table.pack的功能相反。pcak把参数列表转换成Lua语言中一个真实的列表,而unpack则把lua语言中的真实的列表转换成一组返回值,进而可以作为另一个函数的参数被使用。

print(table.unpack{10, 20, 30})   -->  10  20  30
a, b = table.unpack({10,20,30})   -->  a = 10, b = 20, 30被丢弃

正确的尾调用

Lua语言是支持尾调用消除的,这意味着Lua语言可以正确地尾递归,虽然尾调用消除的概念并没有直接涉及递归。 尾调用是被当作函数调用使用的跳转。当一个函数的最后一个动作是调用另外一个函数而没有再进行其他工作时,就形成了尾调用。例如:

function f(x) x = x + 1; return g(x) end

当函数f调用完函数g之后,f不再需要进行其他的工作。这样,当被调用的函数执行结束后,程序就不再需要返回最初的调用者。因此,在尾调用之后,程序也就不需要在调用栈中保存有关调用函数的任何信息。当g返回时,程序的执行路径会直接返回到调用f的位置。在一些语言的实现中,例如Lua语言解释器,就利用了这个特点,使得在进行尾调用时不使用任何额外的栈空间。我们就将这种实现称为尾调用消除( tail-call elimination )

关于尾调用消除的一个重点就是如何判断一个调用是尾调用。很多函数调用之所以不上尾调用,是由于这些函数在调用之后还进行了其他工作,例如,下例中的调用g就不是尾调用:

function f(x) g(x) end

这个实例问题在于,当调用玩g后,f在返回前还是不得不丢弃g返回的结果。类似的,以下实例也都不符合尾调用的定义:

return g(x) + 1   --必须进行加法
return x or g(x)  --必须把返回值限制为1个
return (g(x))     --必须把返回值限制为1个