Lua中函数定义的常规语法,以下例说明之:
function incCount(n)
n = n or 1
count = count + n
end
一个函数定义包括函数名称、参数列表和函数体。这个函数用于递增一个全局的计数器,其中,count是全局变量。需要注意的是:形参与实参之间的赋值关系同多重赋值,即实参多于形参时舍弃多余实参,实参不足则多余的形参初始化为nil。
例子中的函数用了个小技巧,即该函数以1作为默认实参,这是因为当调用函数时,Lua先将参数n初始化为nil,而接下来的or又返回了其第二个操作数,最终Lua将默认值1赋予了n。
一、多重返回值
Lua函数一个显著的特征就是允许函数返回多个值。Lua会调整一个函数的返回值数量以适应不同的调用情况。若将函数调用作为一条单独语句时,Lua会丢弃函数的所有返回值。若将函数作为表达式的一部分来调用时,Lua只保留函数的第一个返回值。只有当一个函数调用是一系列表达式中的最后一个元素(或仅有一个元素)时,才能获得它的所有返回值。这里所谓的“一系列表达式”在Lua中表现为4中情况:多重赋值、函数调用时传入的实参列表、table的构造式和return语句。
为了说明这4中情况,假设有如下函数定义:
function foo() return "a", "b", "c" end --返回3个结果
在多重赋值中。若一个函数调用是最后的(或仅有的)一个表达式,那么Lua会保留其尽可能多的返回值,用于匹配赋值变量:
x, y, z= 10, foo() --x=10, y="a", z="b"
当函数调用作为另一个函数调用的最后一个(或仅有的)实参时,第一个函数的所有返回值都将作为实参传入第二个函数:
print(foo()) --输出a b c
table构造式在函数作为最后一个元素时可以完整地接收一个函数调用的所有结果,如果在其它位置上函数调用只产生一个结果值:
t = {foo()} --t={"a", "b", "c"}
t = {foo(), 4} --t={"a", 4}
return语句返回函数的所有返回值:
function test()
return foo()
end
print(test()) --a, b, c
也可以将函数调用放入一对圆括号中,从而迫使它只返回一个结果:
print((foo())) --a
需要注意的是:return语句后面的内容无须加圆括号,否则将只返回一个值。
二、变长参数
变长参数即variable number of arguments,Lua中的函数可以接受不同数量的实参,比如:
function add(...)
local s = 0
for i, v in ipairs(...) do
s = s + v
end
return s
end
上面函数返回所有参数的总和,参数表中的3个点...表示该函数可接受不同数量的实参。如果函数树妖访问变长参数时,3个点可以作为表达式来使用,表达式...表示一个由所有变长参数构成的数组,遍历该数组就能访问变长参数了。也可以把表达式...的行为看成是一个具有多重返回值的函数,其返回的是当前函数的所有变长参数:
local a, b = ...
上面用第一个和第二个变长参数分别初始化两个局部变量。
Lua提供了专门用于格式化文本(string.format)和输出文本(io.write)的函数,比如:
function fwrite(fmt, ...)
return io.write(string.format(fmt, ...))
end
注意在3个点前有一个固定参数fmt。具有变量参数的函数同样也可以拥有任意数量的固定参数,但固定参数必须放在变长参数之前。Lua会将前面的实参赋予固定参数,而将余下的实参视为变长参数,比如:
fwrite() --fmt=mil
fwrite("a") --fmt="a"
fwrite("%d%d", 4, 5) --fmt=4, 变长参数为5
如果变长参数中含所有nil,则必须要用函数select来访问变长参数。调用select时,必须传入一个固定实参selector(选择开关)和一系列变长参数。如果selector为数字n,那么select返回它的第n个可变实参,否则,selector只能为字符串“#”,这样select会返回变长参数的总数,比如:
for i=1, select('#', ...) do
local arg = select(i, ...) --得到第i个参数
end
特别需要指出的是,select('#', ....)会返回所有变长参数的总数,其中包括nil。
三、具名参数
具名参数即named arguments,一般函数调用时,实参与形参通过位置来匹配,但是有时候在函数参数很多时而参数本身又不具备明显的字面意义时,如果能够根据名称来指定实参那就酷毙了,在Lua中具名参数就能达到这种牛逼闪闪的效果。Lua可以将所有实参组织到一个table中,并将这个table作为唯一的实参传递给函数,此外,当实参只有一个table构造式时,函数调用中的圆括号可有可无,看如下重命名文件名称的函数:
rename{odl="temp.lua", new="temp1.lua"}
function rename(arg)
return os.rename(arg.old, arg.new) --标准库中的OS库
end
若一个函数拥有大量参数,而其中大部分参数都是可选的,则这种参数传递风格会特别有用。比如:在一个GUI库中,一个用于创建新窗口的函数可能会具有许多参数,而其中大部分都是可选的,那么最好使用具名实参:
w = Window{x=0, y=0, width=300, height=200, title="Lua", background="blue", border=true}
Window函数可以根据要求检查必填参数,或者为某些参数添加默认值,而真正的创建窗口的操作在函数内由被调用函数_Window来完成,其要求所有参数以正确的次序传入,那么Window函数可以如此这般:
function Window(option)
--检查必要的参数
if type(option.title) != "string" then
error("no title")
elseif type(options.width) ~= "number" then
error("no width")
elseif type(options.height) ~= "number" then
error("no height")
end
--其它参数可选
_Window(options.title,
options.x or 0 --默认值
options.y or 0 --默认值
options.width,
options.height,
options.background or "white", --默认值
options.border --默认是为false(nil)
)
end
四、高阶函数
在Lua中,函数是一种“第一类值(First-Class Value)”,它们具有特定的词法域(Lexical Scoping)。
第一类值表示Lua中函数与其它传统类型的值(比如:数字、字符串等)具有相同的权利。 函数可以存储在变量中或者table中,也可以作为实参传递给其它函数,还可以作为其它函数的返回值。
词法域是指一个函数可以嵌套在另一个函数中,内部的函数可以访问外部函数中的变量。
在Lua中有一个容易混淆的概念是函数与所有其它值一样都是匿名的,即它们都没有名称。当讨论函数名时(比如:print),实际上是在讨论一个持有某函数的变量,这与其它类型变量持有各种值一个道理,可以以多种方式来操作这些变量:
a = {p=print}
a.p("hello world") -->hello world
print = math.sin -->print现在引用了正弦函数
a.p(print(1)) -->0.841470
在Lua中最常见的函数编写方式如下:
function foo(x) return 2*x end
只是一种所谓的“语法糖”而已,其只是以下代码的一种简写形式:
foo = function (x) return 2*x end
因此,可以将函数看成是一条赋值语句,这条语句创建了一种类型为“函数”的值,并将这个值赋予一个变量。
可以将表达式“function (x) end”视为一种函数的构造式,就像table的构造式{}一样,这种函数构造式的结果就是一个“匿名函数”。
table库提供了一个函数table.sort,该函数如果只接受一个table并对其中的元素进行排序的话,那么规则就是固定的升序或者降序之类的,但是实际中用户的排序规则多种多样,为了解决那这个问题,其提供一个可选的参数用于指定排序规则,该参数是一个函数即次序函数(order function):
table.sort(table_xxx, function (a, b) return (a.key > b.key) end)
像sort这样的函数,接受另一个函数作为实参,这种函数就是高阶函数(higher-order function)。高阶函数只是函数为第一类值这一观点的具体应用。下面举一个高阶函数的实例,即一个求导的高阶函数:
function derivative(f, delta)
delta = delta or 1e-4
return function (x)
return (f(x+delta) - f(x))/delta
end
end
五、closure
closure是基于第一类函数和词法域原理的,下面举一个简单的例子来说明之:
function newCounter()
local i = 0
return function() --匿名函数
i = i + 1
return i
end
end
c1 = newCounter()
print(c1()) -->1
print(c1()) -->2
在上面的代码中,匿名函数访问了一个“非局部变量(non-local variable)”i,该变用于保持一个计数器。咋看上去觉得不可思议:在调用完newCounter函数后,变量i的作用域就应该结束了。Lua会以closure的概念来正确地处理这种情况。简单讲:一个closure就是一个函数加上该函数所需访问的所有“非局部变变量”。每调用一次newCounter,就会创建一个新的局部变量,从而也得到一个新的closure:
c2 = newCounter()
print(c2()) -->1
print(c1()) -->3
print(c2()) -->2
因此,c1和c2是同一个函数创建的两个不同的closure,它们各自拥有局部变量i的独立实例。
从技术上讲,Lua中只有closure,而不存在函数,因为函数本身也是一种特殊的closure。closure是指一个函数及一系列这个函数会访问到“非局部变量或者upvalue”。因此,若一个closure没有那些会访问的“非局部变量的”,那么它就是一个传统概念中的函数。
可以利用Lua的closure来实现sandbox(沙箱),即当执行一些未受信任的代码时为此创建的一个安全的运行环境,这一技术在时下火热的云计算中非常重要。
举例说明:假设服务器要执行一些从Internet上接收的代码,比如:如果要限制一个程序访问文件的话,只需使用closure来重新定义函数io.open就可以了:
do
local oldopen = io.open
local access_OK = function(filename, mode)
<检查访问权限>
end
io.open = function(filename, mode)
if access_OK(filename, mode) then
return oldOpen(filename, mode)
else
return nil, "access denied"
end
end
end
经过重新定义后,一个程序就只能通过新的受限版本来调用原来那个未受限的open函数了,即将原来不安全的版本保存到closure的一个私有变量中,从而使得外部再也无法直接访问到原来的版本了。通过这种技术,可以在Lua的语言层面上就构建一个安全的运行环境,且不失简易性和灵活性。
六、非全局函数
第一类值属性保证了函数不仅可以存储在全局变量中,还能存储在table的字段中和局部变量中。比如:大部分Lua库也采用table才存储函数(比如:io.read、math.sin等)。当然你也可以应用这种机制:
Lib = {}
Lib.foo = function(x, y) return x+y end
Lib.goo = function(x, y) return x-y end
只要将一个函数存储到一个局部变量中,即得到了一个“局部函数(Local function)”,也就是说该函数只能在某个特定的作用域中使用。
在定义递归的局部函数时,需要先定义一个局部变量,然后再定义函数本身,具体如下:
local fact -- 1
fact function(n) -- 2
if n == 0 then return 1
else return n*fact(n-1) -- 4
end
end
如果将1和2两行合并为local fact = function(n),则4行则会出现调用错误,因为此时局部fact尚未定义,而表达式其实调用的是一个全局fact而非函数自身。
也可以将上面函数改良为如下形式:
local function fact(n)
if n == 0 then return 1
else return n*fact(n-1)
end
end
但是上面的函数不能用于间接递归调用,为了解决这个问题必须使用一个明确的前向声明(Forward Declaration):
local f, g --前向声明
function g()
<一些代码> f() <一些代码>
end
function f()
<一些代码> g() <一些代码>
end
需要注意的是:别把第二个函数定义写为“local function f”,否则,Lua会创建一个全新的局部变量f,而将原来声明的f(函数g中所引用的那个)置于未定义状态。
七、尾调用
Lua中函数支持“尾调用消除(tail-call elimination)”,即Lua能正确地处理函数结尾时的递归调用。
所谓“尾调用(tail call)”就是一种类似于goto的函数调用,即当一个函数调用是另一个函数的最后一个动作时,该调用才算是一条“尾调用”。比如:以下代码中对g的调用就是一条“尾调用”:
function f(x) return g(x) end
也就是说,当f调用完g之后就无事可做了,因此,在这种情况下,程序就不需要返回那个“尾调用”所在的函数了,所以,在“尾调用”之后,程序也就需要保存任何关于该函数的栈(stack)信息了。当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) --必须调整为一个返回值
return (g(x)) --必须调整为一个返回值
在Lua中,只有”return
()”这样的调用形式才算是一条”尾调用“。Lua会在调用前对及其参数求值,所以它可以是任何复杂的表达式,比如:下面的调用就是一条”尾调用“:
return x[i].foo(x[j]+a*b, i+j)
一条”尾调用“就好比是一条goto语句,因此,”尾调用“的一大应用就是编写”状态机(state machine)“。这种程序通常以一个函数来表示一个状态,改变状态就是goto(或调用)到另一个特定的函数。迷宫问题就是典型应用,当前房间是一个状态,用每次尾调用来实现从一个房间移动到另一个房间。