在Lua语言中,函数是对语句和表达式进行抽象的主要方式。函数既可以用于完成某种特定任务,也可以只是进行一些计算然后返回计算结果。在前一种情况下,我们将一句函数调用视为一条语句;而在后一种情况下,我们则将函数调用视为表达式:
1 | print(8*9 , 9/8) |
无论哪种情况,函数调用时都需要使用一对圆括号把参数列表括起来。即使被调用的函数不需要参数,也需要一对空括号()。对于这个规则,唯一的例外就是,当函数只有一个参数且该参数是字符串常量或表构造器时:
1 | print "Hello World" <--> print("Hello World") |
Lua语言也为面向对象风格的调用提供了一种特殊的语法,即冒号操作符。形如x:foo(x)的表达式意味为调用对象o的foo方法。
一个Lua程序既可以调用Lua语言编写的函数,也可以调用C语言编写的函数。一般来说,我们选择使用C语言编写的函数来实现对性能要求更高,或不容易直接通过Lua语言进行操作的操作系统机制等。例如,Lua语言标准库中所有的函数就都是使用C语言编写的。不过,无论一个函数是用Lua语言编写的还是用C语言编写的,在调用它们时都没有任何区别。
正如我们已经在其他示例中所看到的,Lua语言中的函数定义的常见语法格式形如:
1 | function add( a ) |
这种语法中,一个函数定义具有一个函数名、一个参数组成的列表和由一组语句组成的函数体。参数的行为与局部变量的行为完全一致,相当于一个用函数调用时转入的值进行初始化的局部变量。
调用函数时使用的参数个数可以与定义函数时使用的参数个数不一致。Lua语言会通过抛弃多余参数和将不足的参数设为nil的方式来调整参数的个数。例如,考虑如下的函数:
1 | function f (a,b) print(a , b) end |
其形为如下:
1 | f() -- nil nil |
虽然这种行为可能导致编程错误,但同样又是有用的,尤其是对于默认参数的情况。例如,考虑如下递增全局计数器的函数:
1 | function incCount( n ) |
该函数以1作为默认实参,当调用无参数的incCount()时,将globalCounter加1。在调用incCount()时,Lua语言首先把参数n初始化为nil,接下来or表达式又返回了其第二个操作数,最终把n赋成了默认值1。
多返回值
Lua语言中一种与众不同但又非常有用的特性是允许一个函数返回多个结果。Lua语言中几个预定义函数就会返回多个值。我们已经接触过函数string.find,该函数用于在字符串中定位模式。当找到了对应的模式时,该函数会返回两个索引值:所匹配模式在字符串中初始字符和结尾字符的索引。使用多重赋值可以同时获取到这两个结果:
1 | s, e = string.find("hello lua users" , "Lua") |
请记住,字符串的第一个字符的索引值为1。
Lua语言编写的函数同样可以返回多个结果,只需在return关键字后列出所有要返回的值即可。例如,一个用于查找序列中最大元素的函数可以同时返回最大值及该元素的位置:
1 | function maximum(a) |
Lua语言根据函数的被调用情况调整返回值的数量。当函数被作为一条单独语句调用时,其所有返回值都会被丢弃;当函数被作为表达式调用时,将只保留函数的第一个返回值。只有当函数调用是一系列表达式中的最后一个表达式时,其所有的返回值才能被获取到。这里所谓的“一系列表达式”在Lua中表现为4种情况:多重赋值、函数调用时传入的实参列表、表构造器和return语句。为了分别展示这几种情况,接下来举几个例子:
1 | function foo0() end -- 不返回结果 |
在多重赋值中,如果一个函数调用是一系列表达式中的最后一个表达式,则该函数调用将产生尽可能多的返回值以匹配待赋值变量:
1 | x,y = foo2() -- x = "a", y = "b" |
在多重赋值中,如果一个函数没有返回值或者返回值个数不够多,那么Lua语言会用nil来补充缺失的值:
1 | x,y = foo0() -- x = nil , y = nil |
请注意,只有当函数调用一系列表达式中的最后一个表达式时才能返回多值结果,否则只能返回一个结果:
1 | x,y = foo2(), 20 -- x = "a", y = 20 ('b'被丢弃) |
当一个调用是另一个函数调用的最后一个实参时,第一个函数的所有返回值都会被作为实参传给第二个函数。我们已经见到过很多这样的代码结构,例如函数print。由于函数print能够接收可变数量的参数,所以print(g())会打印出g返回的所有结果。
1 | print(foo0()) -- 没有结果 |
当在表达式中调用foo2时,Lua语言会把其返回值的个数调整为1.因此,在上例的最后一行,只有第一个返回值”a”参与了字符串连接操作。
当我们调用f(g())时,如果f的参数是固定的,那么Lua语言会把g返回值的个数调整成与f的参数个数一致。
表构造器会完整地接收函数调用的所有返回值,而不会调整返回值的个数:
1 | t = {foo0()} -- t = {} |
不过,这种行为只有当函数调用是表达式列表中的最后一个时才有效,在其他位置上的函数总是只返回一个结果:
1 | t = {foo0(),foo2(),4} -- t[1] = nil, t[2] = "a", t[3] = 4 |
最后,形如return f()的语句会返回f返回的所有结果:
1 | function foo(i) |
将函数调用用一对圆括号括起来可以强制其只返回一个结果:
1 | print(foo0()) -- nil |
应该意识到,return语句后面的内容是不需要加括号的,如果加了括号会导致程序出现额外的行为。因此,无论f究竟返回几个值,形如return(f(x))的语句只返回一个值。又是这可能是我们所希望出现的情况,但有时又可能不是。
可变长参数函数
Lua语言中的函数可以是可变长参数函数,即可以支持数量可变的参数。例如,我们已经使用一个、两个或多个参数调用过函数print。虽然函数print是在C语言中定义的,但也可以在Lua语言中定义可变长参数函数。
下面是一个简答的示例,该函数返回所有参数的总和:
1 | function add (...) |
参数列表中的三个点(…)表示该函数的参数是可变长的。当这个函数被调用时,Lua内部会把它所有参数收集起来,我们把这些被收集起来的参数称为函数的额外参数。当函数要访问这些参数时仍需用到三个点,但不同的是此时这三个点是作为一个表达式来使用的。在上例中,表达式{…}的结果是一个由所有可变长参数组成的列表,该函数会遍历该列表来累加其中的元素。
我们将三个点组成的表达式称为可变长参数表达式,其行为类似于一个具有多个返回值的函数,返回的是当前函数的所有可变长参数。
实际上,可以通过变长参数来模拟Lua中普遍的参数传递机制,例如:
1 | funtion foo (a,b,c) |
可以写成
1 | function foo(...) |
喜欢Perl参数传递机制的人可能会更喜欢第二种形式。
形如下列的函数只是将调用它时所传入的所有参数简单地返回:
1 | function id (...) return ... end |
该函数是一个多值恒等式函数。下列函数的行为则类似于直接调用函数foo,唯一不同之处是在调用函数foo之前会先打印出传递函数foo的所有参数:
1 | function foo1( ... ) |
当跟踪对某个特定的函数调用时,这个技巧很有用。
接下来再让我们看另外一个很有用的示例。Lua语言提供了专门用于格式化输出的函数string.format和输出文本的函数io.write。我们会很自然地想到把这两个函数合并为一个具有可变长参数的函数:
1 | function fwirte(fmt, ...) |
注意,在三个点前游一个固定的参数fmt。具有可变长参数的函数也可以具有任意数量的固定参数,但固定参数必须放在变长参数之前。Lua语言会先将前面的参数赋给固定参数,然后将剩余的参数作为可变长参数。
要遍历可变长参数,函数可以使用表达式{…}将可变长参数放在一个表中,就像add示例中所作的那样。不过,在某些罕见的情况下,如果可变长参数中包含无效的nil,那么{…}获得的表可能不再是一个有效的序列。此时,就没有办法在表中判断原始参数究竟是不是以nil结尾的。对于这种情况,Lua语言提供了函数table.pack。该函数像表达式{…}一样保存所有的参数,然后将其放在一个表中返回,但是这个表还有一个保存了参数个数的额外字段”n”。例如,下面的函数使用了函数table.pack来检测参数中是否有nil:
1 | function nonils(...) |
另一种遍历函数的可变长参数的方法是使用函数select。函数select总是具有一个固定的参数select,以及数量可变的参数。如果select是数值n,那么函数select则返回第n个参数后的所有参数;否则,select应该是字符串”#”,以便函数select返回额外参数的总数。
1 | print(select(1,"a","b","c")) -- a b c |
通常,我们在需要把返回值个数调整为1的地方使用函数select,因此可以把select(n,…)认为是返回第n个额外参数的表达式。
来看一个使用函数select的典型示例,下面是使用该函数的add函数:
1 | function add(...) |
对于参数较少的情况,第二个版本的add更快,因为该版本避免了每次调用时创建一个新表。不过,对于参数较多的情况,多次带有很多参数调用函数select会超过创建表的开销,因此第一个版本会更好。
函数table.unpack
多重返回值还涉及一个特殊的函数table.unpack。该函数的参数是一个数组,返回值为数组内的所有元素:
1 | print(table.unpack{10,20,30}) -- 10 20 30 |
顾名思义,函数table.unpack与函数table.pack的功能相反。pack把参数列表转换成Lua语言中一个真实的列表,而unpack则把Lua语言中的真实的列表转换成一组返回值,进而可以作为另一个函数的参数被使用。
unpack函数的重要用途之一体现在泛型调用机制中。泛型调用机制允许我们动态地调用具有任意参数的函数。例如,在IOS C中,我们无法编写泛型调用的代码,只能声明可变长参数的函数或使用函数指针来调用不同的函数。但是,我们仍然不能调用具有可变量参数的函数,因为C语言中的每一个函数调用的实参个数是固定的,并且每个实参的类型也是固定的。而在Lua语言中,却可以做到这一点。如果我们想通过数组a传入可变的参数来调用函数f,那么可以写成:
1 | f(table.unpack(a)) |
unpack会返回a中所有的元素,而这些元素又被用作f的参数。例如,考虑如下的代码:
1 | print(string.find("hello","ll")) |
可以使用如下的代码动态地构造一个等价的调用:
1 | f = string.find |
通常,函数table.unpack使用长度操作符获取返回值的个数,因而该函数只能用于序列。不过,如果有需要,也可以显示地限制返回元素的范围:
1 | print(table.unpack({"Sun","Mon","Tue","Wed"},2,3)) -- Mon Tue |
虽然预定义的函数unpack是用C语言编写的,但是也可以利用递归在Lua语言中实现:
1 | function unpack(t,i,n) |
在第一次调用该函数时,只传入一个参数,此时i为1,n为序列长度;然后,函数返回t[1]及unpack(t,2,n)返回的所有结果,而unpack(t,2,n)又会返回t[2]及unpack(t,3,n)返回的所有结果,一次类推,直到处理完n个元素为止。
正确的尾调用
Lua语言中有关函数的另一个有趣的特性是,Lua语言是支持尾调用消除的。这意味着Lua语言可以正确地尾递归,虽然尾调用消除的概念并没有直接涉及递归。
尾调用是被当作函数调用使用的跳转。当一个函数的最后一个动作是调用另一个函数而没有再进行其他工作时,就行程了尾调用。例如,下例代码中对函数g的调用就是尾调用:
1 | function f(x) x = x + 1;return g(x) end |
当函数f调用完函数g之后,f不再需要进行其他的工作。这样,当被调用的函数执行结束后,程序就不再需要返回最初的调用者。因此,在尾调用后,程序也就不需要在调用栈中保存有关调用函数的任何信息。当g返回时,程序的执行路径会直接返回到调用f的位置。在一些语言的实现中,例如Lua语言解释器,就利用了这个特点,是的进行尾调用时不使用任何额外的栈空间。我们就将这种实现称为尾调用消除。
由于尾调用不会使用栈空间,所以一个程序中能够嵌套的尾调用的数量是无限的。例如,下例函数支持任意的数字作为参数:
1 | function foo (n) |
该函数永远不会发生栈溢出。
关于尾调用消除的一个重点就是如何判断一个调用是尾调用。很多函数之所有不是尾调用,是由于这些函数在调用之后还进行了其他工作。例如,下例中调用g就不是尾调用:
1 | function f(x) |
这个示例的问题在于,当调用完g后,f在返回前还不得不丢弃g返回的所有结果。类似的,以下的所有调用也都不符合尾调用的定义:
1 | return g(x) + 1 -- 必须进行加法 |
在lua语言中,只有形如return func(args)的调用才是尾调用。不过,由于Lua语言会在调用钱对func及其参数求值,所以func及其参数都可以是复杂的表达式。例如,下面的例子就是尾调用:
1 | return x[i].foo(x[j] + a * b, i + j) |