Lua 第6部分 函数

发布于:2025-04-20 ⋅ 阅读:(37) ⋅ 点赞:(0)

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

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 message]]	<-->	print([[a multi-line 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 语言编写的还是用 C语言编写的 , 在调用它们时都没有任何区别 。

        正如我们已经在其他示例中所看到的, Lua 语言中的函数定义的常见语法格式形如 :

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

在这种语法中 ,一个函数定义具有一个函数名(name ,本例中的 add) 、 一个参数( 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

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

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

该函数以 1 作为默认实参,当调用无参数的 incCount() 时,将 globalCounter 加 1 。 在调用
incCount() 时, Lua 语言首先把参数 n 初始化为 nil ,接下来的 or 表达式又返回了其第二个操作数,最终把 n 赋成了默认值 1 。

6.1  多返回值

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

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

请记住,字符串的第一个字符的索引值为1

        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

> print(maximum({8,10,23,12,4}))
23      3

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

function foo0 () end					-- 不返回结果
function foo1 () return "a" end			-- 返回1个结果
function foo2 () return "a", "b" end	-- 返回2个结果

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

> x,y=foo2()
> print(x,y)
a       b
> x=foo2()
> print(x)
a
> x,y,z=10,foo2()
> print(x,y,z)
10      a       b

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

> x,y=foo0()
> print(x,y)
nil     nil
> x,y=foo1()
> print(x,y)
a       nil
> x,y,z=foo2()
> print(x,y,z)
a       b       nil

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

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

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

> print(foo0())

> print(foo1())
a
> print(foo2())
a       b
> print(foo2(), 1)
a       1
> print(foo2().."x")
ax

当在表达式中调用 foo2 时 , Lua 语言会把其返回值的个数调整为 1 。 因此 ,在上例的最后一行,只有第一个返回值” a ” 参与了字符串连接操作。

        当我们调用 f(g() 时如果 f 的参数是固定的,那么 Lua 语言会把 g 返回值的个数调整成与 f 的参数个数一致。 这并非巧合,实际上这正是多重赋值的逻辑
        表构造器完整地接收函数调用的所有返回值 , 而不会调整返回值的个数

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

不过,这种行为只有当函数调用是表达式列表中的最后一个时才有效 ,在其他位置上的函数调用总是只返回一个结果 :

> t={foo0(), foo2(),4}
> print(t[1],t[2],t[3])
nil     a       4

        最后,形如 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

应该意识到, return 语句后面的内容是不需要加括号的 , 如果加了括号会导致程序出现额外的行为 。 因此,无论 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))

参数列表中的三个点( ... )表示该函数的参数是可变长的。 当这个函数被调用时, Lua 内部会把它的所有参数收集起来,我们把这些被收集起来的参数称为函数的额外参数 。 当函数要访问这些参数时仍需用到三个点,但不同的是此时这三个点是作为一个表达式来使用的 。 在上例中,表达式 { ... }的结果是一个由所有可变长参数组成的列表,该函数会遍历该列表来累加其中的元素。

        我们将三个点组成的表达式称为可变长参数表达式,其行为类似于一个具有多个返回值的函数,返回的是当前函数的所有可变长参数。 例如, print( ... ) 会打印出该函数的所有参数。 又如,如下的代码创建了两个局部变量 ,其值为前两个可选的参数(如果参数不存在则为 nil) :

local a, b = ...

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

function foo (a, b, c)

可以写成:

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

喜欢 Perl 参数传递机制的人可能会更喜欢第二种形式。

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

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

该函数是一个多值恒等式函数 。 下列函数的行为则类似于直接调用函数 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 会超过创建表的开销,因此第一个版本会更好(特别地,由于迭代的次数和每次迭代时传人参数的个数会随着参数的个数增长 ,因此第二个版本的时间开销是二次代价的)。

6.3  函数 table.unpack

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

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

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

        unpack 函数的重要用途之一体现在泛型调用机制中 。 泛型调用机制允许我们动态地调用具有任意参数的任意函数。 例如,在 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)))
3       4

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

> print(table.unpack({"Sun","Mon","Tue","Wed"},2,3))
Mon     Tue

        虽然预定义的函数 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, t + 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 语言中有关函数的另一个有趣的特性是 , Lua 语言是支持尾调用消除的 。 这意味着Lua 语言可以正确地尾递归,虽然尾调用消除的概念并没有直接涉及递归。

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

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

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

        由于尾调用不会使用栈空间,所以一个程序中能够嵌套的尾调用的数量是无限的。 例如 ,
下列函数支持任意的数字作为参数 :

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)


网站公告

今日签到

点亮在社区的每一天
去签到