函数既可以用于完成某种特定任务,也可以只是进行一些计算然后返回计算结果。在前一种情况下,我们将一句函数调用视为一条语句;而在后一种情况下,我们则将函数调用视为表达式:

print(8 * 9, 9 / 8)
a = math.sin(3) + math.cos(10) 
print(os.date())

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

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

Lua语言也为面向对象风格的调用(object-oriented call)提供了一种特殊的语法,即冒号操作符,形如o:foo(x)的表达式意为调用对象o的foo方法。

一个Lua程序既可以调用Lua语言编写的函数,也可以调用C语言(或者宿主程序使用的其他任意语言)编写的函数。一般来说,我们选择使用C语言编写的函数来实现对性能要求更高,或不容易直接通过Lua语言进行操作的操作系统机制等。例如,Lua语言标准库中所有的函数就都是使用C语言编写的。

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

调用函数时使用的参数个数可以与定义函数时使用的参数个数不一致。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被丢弃)

虽然这种行为可能导致编程错误(在单元测试中容易发现),但同样又是有用的,尤其是对于默认参数(default argument)的情况。例如,考虑如下递增全局计数器的函数:

function incCount (n)
	n = n or 1
	globalCounter = globalCounter + n
end

n的默认参数是1,当调用无参数的incCount时,将globalCounter加1。

6.1 多返回值

Lua语言中一种特性是允许一个函数返回多个结果(Multiple Results)。Lua语言中有几个预定义函数就会返回多个值。我们已经接触过函数strin、,find,该函数用于在字符串中定位模式(pattern)。当找到了对应的模式时,该函数会返回两个索引值:所匹配模式在字符串中起始字符和结尾字符的索引。使用多重赋值(multiple assignment)可以同时获取到这两个结果:

s, e = string.find("hello Lua users", "Lua")
print(s, e)		--> 7 9

Lua语言编写的函数同样可以返回多个结果,只需在return关键字后列出所有要返回的值即可。例如,一个用于查找序列中最大元素的函数可以同时返回最大值及该元素的位置:

function maximum (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

Lua语言根据函数的被调用情况调整返回值的数量。当函数被作为一条单独语句调用时,其所有返回值都会被丢弃;当函数被作为表达式(例如,加法的操作数)调用时,将只保留函数的第一个返回值。只有当函数调用是一系列表达式中的最后一个表达式(或是唯一一个表达式)时,其所有的返回值才能被获取到。这里所谓的“一系列表达式”在Lua中表现为4种情况:多重赋值、函数调用时传入的实参列表、表构造器和return语句。为了分别展示这几种情况,接下来举几个例子:

function foo0 () end					-- 不返回结果
function foo1 () return "a" end			-- 返回一个结果
function foo1 () return "a", "b" end	-- 返回两个结果

首先是多重赋值,一个函数调用是一系列表达式中的最后(或者唯一)一个表达式,该函数调用将产生尽可能多的返回值来匹配待赋值变量,若不够会用nil来补充,并且不满足时只会产生一个返回值(第一个或nil(无返回值)):

x, y = foo2()				-- x = "a", y = "b"
x = foo2()					-- x = "a", "b"被丢弃
x, y, z = 10, foo2()		-- x = 10, y = "a", z = "b"
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 = foo2(), 20 30		-- x = "a", y = 20 (30被丢弃)

其次是函数调用时传入的实参列表,当一个函数调用是另一个函数调用的最后一个(或者是唯一)实参时,参数中函数的所有返回值都会被作为实参传给第二个函数。我们已经见到过很多这样的代码结构,例如函数 print。由于函数print能够接收可变数量的参数,所以print(g())会打印出g返回的所有结果。

当我们调用f(g())时,如果f的参数是固定的,那么Lua语言会把g返回值的个数调整成与f的参数个数一致。这并非巧合,实际上这正是多重赋值的逻辑。

接着是表构造器,表构造器会完整地接收函数调用的所有返回值,而不会调整返回值的个数:

t = {foo0()}		-- t = {} (一个空表)
t = {foo1()}		-- t = {"a"}
t = {foo2()}		-- t = {"a", "b"}

不过,这种行为依旧要满足当函数调用是表达式列表中的最后(或者唯一)一个时才有效,在其他位置总只返回一个结果。

t = {foo0(), foo2(), 4}		-- t = {nil, "a", 4}

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

function foo (i)
    if i == 0 then return foo0()
    elseif i == 1 then return 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

因此,无论f返回几个值,return (f(x))只返回一个值。

6.2 可变长参数函数

Lua语言中的函数可以是可变长参数函数(variadic),即可以支持数量可变的参数。例如,我们已经使用一个、两个或更多个参数调用过函数print。虽然函数print是在C语言中定义的,但也可以在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内部会把它的所有参数收集起来,我们把这些被收集起来的参数称为函数的额外参数(extra argument)。当函数要访问这些参数时仍需用到三个点,但不同的是此时这三个点是作为一个表达式来使用的。在上例中,表达式{…}的结果是一个由所有可变长参数组成的列表,该函数会遍历该列表来累加其中的元素。我们将三个点组成的表达式称为可变长参数表达式(vararg expression),其行为类似于一个具有多个返回值的函数,返回的是当前函数的所有可变长参数。例如,print(…)会打印出该函数的所有参数。又如,如下的代码创建了两个局部变量,其值为…中前两个可选的参数(如果不存在则为nil):

local a, b = ...

实际上,可以通过变长参数来模拟Lua语言中普通的参数传递机制,例如:

function foo (a,b,c)

可以写成:

function foo (...)
    local a, b, c = ...

形如下例的函数只是将调用它时所传人的所有参数简单地返回:

function id (...) return ... end

该函数是一个多值恒等式函数(multi-value identity function)。下列函数的行为则类似于直接调用函数foo,唯一不同之处是在调用函数foo之前会先打印出传递给函数foo的所有参数:

function foo1 (...)
	print("calling foo:", ...)
    return foo(...)
end

当跟踪对某个特定的函数调用时,这个技巧很有用。

接下来再让我们看另外一个很有用的示例。Lua语言提供了专门用于格式化输出的函数string.format和输出文本的函数io.write。我们会很自然地想到把这两个函数合并为一个具有可变长参数的函数:

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

注意,在三个点前有一个固定的参数fmt。具有可变长参数的函数也可以具有任意数量的固定参数,但固定参数必须放在变长参数之前。Lua语言会先将前面的参数赋给固定参数,然后将剩余的参数(如果有)作为可变长参数。

要遍历可变长参数,函数可以使用表达式{…}将可变长参数放在一个表中,就像add示例中所做的那样。不过,在某些罕见的情况下,如果可变长参数中包含无效的nil,那么{…}获得的表可能不再是一个有效的序列。此时,就没有办法在表中判断原始参数究竟是不是以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。函数select总是具有一个固定的参数selector,以及数量可变的参数。如果selector是数值n,那么函数select则返回第n个参数及以后的所有参数;否则,selector应该是字符串"#",以便函数select返回额外参数的总数。

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

通常,我们在需要把返回值个数调整为1的地方使用函数select,因此可以把select(n, …)认为是返回第n个额外参数的表达式。来看一个使用函数select的典型示例,下面是使用该函数的add函数:

function add (...)
	local s = 0
    for i = 1, select("#", ...)do
        s = s + select(i,...)
	end
    return s
end

对于参数较少的情况,第二个版本的add更快,因为该版本避免了每次调用时创建一个新表。不过,对于参数较多的情况,多次带有很多参数调用函数select会超过创建表的开销,因此第一个版本会更好(特别地,由于迭代的次数和每次迭代时传入参数的个数会随着参数的个数增长,因此第二个版本的时间开销是二次代价(quadratic cost)的)。

6.3 函数table.unpack

多重返回值还涉及一个特殊的函数table.unpack。该函数的参数是一个数组(表),返回值为数组内的所有元素:

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

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

unpack函数的重要用途之一体现在泛型调用(generic call)机制中。泛型调用机制允许我们动态地调用具有任意参数的任意函数。例如,在ISO C中,我们无法编写泛型调用的代码,只能声明可变长参数的函数(使用stdarg.h)或使用函数指针来调用不同的函数。但是,我们仍然不能调用具有可变数量参数的函数,因为C语言中的每一个函数调用的实参个数是固定的,并且每个实参的类型也是固定的。而在Lua语言中,却可以做到这一点。如果我们想通过数组a传入可变的参数来调用函数f,那么可以写成:

f(table.unpack(a))

unpack会返回a中所有的元素,而这些元素又被用作f的参数。例如,考虑如下的代码:

print(string.find("hello", "ll"))

可以使用如下的代码动态地构造一个等价的调用:

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

print(f(table.unpack(a)))

通常,函数table.unpack使用长度操作符获取返回值的个数,因而该函数只能用于序列。不过,如果有需要,也可以显式地限制返回元素的范围:

print(table.unpack({"Sun", "Mon", "Tue", "Wed"}, 2, 3))		--> Mon Tue 2和3是起始和终止的位置

虽然预定义的函数unpack是用C语言编写的,但是也可以利用递归在Lua语言中实现:

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

在第一次调用该函数时,只传入一个参数,此时i为1,n为序列长度;然后,函数返回t[1]及unpack(t, 2, n)返回的所有结果,而unpack(t, 2, n)又会返回t[2]及unpack(t, 3, n)返回的所有结果,依此类推,直到处理完n个元素为止。

6.4 正确的尾调用

Lua支持尾调用消除(tail-call elimination)。这意味着Lua可以正确地(properly)尾递归(tail recursive),虽然尾调用消除的概念并没有直接涉及递归。

尾调用(tail call)是被当作函数调用使用的跳转。当一个函数的最后一个动作是调用另一个函数而没有再进行其他工作时,就形成了尾调用。例如,下列代码中对函数g的调用就是尾调用:

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

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

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

该函数永远不会发生栈溢出。

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

function f (x)
	g(x)
end

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

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

在Lua语言中,只有形如return func(args)的调用才是尾调用。不过,由于Lua语言会在调用前对func及其参数求值,所以func及其参数都可以是复杂的表达式。例如,下面的例子就是尾调用:

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