Lua函数
回到:
函数
Lua函数是一段可执行、可被调用的代码块,在Lua中函数是first class,它可以作为值赋值给变量,作为值传递给参数,也可以作为值被函数返回。
Lua定义函数:
1 | function f(x,y) |
调用函数时,加上括号并传递参数即可执行。
函数调用时,如果参数只有一个,且这个参数是一个字符串字面量或者是table字面量(即table构造式),则可省略括号。
1 | f "hello" --> f("hello") |
函数返回值
函数使用return来返回,return(和break一样)必须在语句块的结尾,每个函数在结尾都有一个隐含的return nil
,且函数可返回多个值。
1 | --> 查找序列中最大的值,同时返回该值的索引 |
因为函数可以在不同环境下被调用,它的返回值会根据如下规则进行调整:
- 如果函数是单独的一条语句,则丢弃所有返回值
- 如果函数是表达式的一部分,则只保留函数第一个返回值
- 只有函数是多个表达式的最后一个元素(或唯一的元素),才能获取函数的所有返回值。包括如下几种情况:
- 函数调用并多重赋值给变量时
- 函数调用并多重赋值给函数形参时
- 作为return的返回值部分时
- 作为table的构造语句时
- 使用小括号包围函数调用(如
(f())
),可强制返回第一个返回值(所有要注意return语句中的函数调用不能加括号,否则只会返回单个值)
例如:
1 | function f() return "a", "b", "c" end |
函数参数
函数形参数量和实参数量可以不一致,会按照变量赋值一样的方式进行调整。
1 | function f(x,y,z) print(x,y,z) end |
如果需要在函数内部验证是否传递了某参数,或者为某参数提供默认值,可参考如下:
1 | function f(x) |
如果有多个参数要传递,可以将参数收集在table中,然后传递table,或者使用table.unpack()解包table。
1 | function f(x,y,z) return x, y, z end |
变长参数
形参使用...
可以接收剩下的所有参数:
1 | function f(x,y,z,...) |
在非形参为位置,...
表示的是一个表达式,正如上面{...}
,类似于多返回值的函数一样,被构造成一个序列,该序列中包含了变长参数符号...
接收到的所有实参。
1 | function x(...) print(...) end |
有时候有些参数可能会传递nil值作为其实参,但nil值会破坏对...
的遍历,这时可使用select()
。
使用select(index, ...)
可以处理变长参数表达式...
,当index指定为字符”#”时,它将返回...
的总长度,即接收到的变长参数数量,如果index是一个整数值,则返会该整数值为索引的元素以及其后的所有元素,index可以为负数。
1 | function f(...) return select("#",...) end |
使用table.pack(...)
可以将变长参数打包成一个table,该table还包含了一个名为”n”的key,其值为打包的参数数量。
1 | function f(...) |
例如,定义一个函数,返回所有参数之和:
1 | function add(...) |
参数默认值
有些语言中允许在参数列表中定义函数的默认值,例如:
1 | function f(name="long",age=23) |
Lua不直接支持这种定义方式,但是Lua的函数调用有一个特性:当只有一个参数且参数为字符串字面量或table字面量时,可以省略括号。
所以,可以以table作为实参的方式写成如下类似的函数定义:
1 | function f(arg) |
然后调用时就可以使用如下方式调用:
1 | f{name="junmajinlong",age=23} |
尾调用消除
Lua原生支持尾调用消除(tail call elimination),使得在递归的时候可以直接尾递归(tail recursive)。
例如,下面的哈数:
1 | function f(x) |
在上述示例中,函数f()内部调用了函数g(),且调用g()的时刻是f()的最后一个动作,当从g()执行完成返回到f(),f()不会做出任何事,而是直接退出回到调用f()的地方。
所以,对于f()内部调用g()来说,如果调用g()是f()的最后一个动作,那么调用g()之后其实无需保留f()的栈帧(因为即使保留了也没有正面作用),可以直接让g()复用f()的栈帧,当g()执行完成后,将直接从g()返回到调用f()的地方。这就是尾调用消除。
但是要注意,调用的g()必须是f()中最后一个操作才算是尾调用,即只有像return g()
一样,最后执行的是return且return中只有函数调用的操作。
例如,下面的示例中g()就不是最后一个操作,调用完g()后,还将等待g()执行完后返回f()进行一次加法操作。
1 | function f(x) |
尾调用消除主要用于尾递归,只要是满足尾调用的递归函数调用,无论递归多少次,都只占用常量的栈帧数量,不会出现栈溢出问题。
例如,不满足尾调用递归的阶乘计算方法:
1 | function fact(n) |
对于这个递归函数,假如执行的是fact(4),它将在各层栈帧中维护如下数据(即保留状态):
1 | fact(4) |
对上面的函数进行改装,将n * fact(n-1)
这个操作想办法将乘积状态保存到函数调用中去:
1 | function fact(n,m) |
为了测试,把上面的阶乘计算改成计算给定数的和。
1 | --> 不使用尾调用 <-- |
并非所有的递归函数都能改装成尾递归调用,也并非所有的语言都原生支持尾调用消除,有些语言可能需要添加额外的编译参数才能打开默认被禁用的尾调用消除功能。特别地,尾调用功能增加了基于栈帧的调试难度。
将函数保存在table中
Lua中函数也是一个值,它可以保存在table中。
例如:
1 | List = {} |
如此定义后,就可以通过List.FuncName来调用对应的函数:
1 | List.push(List,"a") |
局部函数
Lua中函数可以赋值给一个变量,而变量可以是全局变量,也可以是局部变量。
如果将函数赋值给一个局部变量,它将成为局部函数,按照作用域内的局部变量可见性,局部函数将只能在对应作用域内可见。
1 | local f = function(x,y) return x+y end --> (1) |
这两种方式并不完全等价。定义方式(1)是先定义函数,然后赋值给局部变量f,而定义方式(2)是先定义局部变量f,然后定义函数,再将函数赋值给局部变量f。即(2)等价于:
1 | local f |
要注意定义方式(1)的局部函数在递归调用时可能出现的错误:
1 | local fact = function (n) |
因为在编译函数体内的fact(n-1)时,局部变量fact还未定义(总是先评估等号右侧,然后才赋值)。
所以,可改为定义方式(2)的局部函数,或者等价的如下方式定义:
1 | --> local function f <-- |