
文章目录
前言
在 Lua 中,函数是一种对语句和表达式进行抽象的主要机制。函数既可以完成某项特定的任务,也可以只做一些计算并返回结果。在第一种情况中,一句函数调用被视为一条语句;而在第二种情况中,则将其视为一句表达式。
使用函数的好处:
1. 降低程序的复杂性:把函数作为一个独立的模块,写完函数后,只关心它的功能,而不再考虑函数里面的细节。
2. 增加程序的可读性:当我们调用 math.max() 函数时,很明显函数是用于求最大值的,实现细节就不关心了。
3. 避免重复代码:当程序中有相同的代码部分时,可以把这部分写成一个函数,通过调用函数来实现这部分代码的功能,节约空间,减少代码长度。
4. 隐含局部变量:在函数中使用局部变量,变量的作用范围不会超出函数,这样它就不会给外界带来干扰
一、 函数定义
Lua使用关键字function定义函数,语法如下:
function function_name (arc) -- arc 表示参数列表,函数的参数列表可以为空
-- body
end
上面的语法定义了一个全局函数,名为 function_name. 全局函数本质上就是函数类型的值赋给了一个全局变量,即上面的语法等价于
function_name = function (arc)
-- body
end
由于全局变量一般会污染全局名字空间,同时也有性能损耗(即查询全局环境表的开销),因此我们应当尽量使用“局部函数”,其记法是类似的,只是开头加上 local 修饰符:
local function function_name (arc)
-- body
end
由于函数定义本质上就是变量赋值,而变量的定义总是应放置在变量使用之前,所以函数的定义也需要放置在函数调用之前。如果参数列表为空,必须使用 () 表明是函数调用。
由于函数定义等价于变量赋值,我们也可以把函数名替换为某个 Lua 表的某个字段,例如
function foo.bar(a, b, c)
-- body ...
end
此时我们是把一个函数类型的值赋给了 foo 表的 bar 字段。换言之,上面的定义等价于
foo.bar = function (a, b, c)
print(a, b, c)
end
对于此种形式的函数定义,不能再使用 local 修饰符了,因为不存在定义新的局部变量了。
二、 函数的参数
2.1 按值传递
Lua 函数的参数大部分是按值传递的。值传递就是调用函数时,实参把它的值通过赋值运算传递给形参,然后形参的改变和实参就没有关系了。在这个过程中,实参是通过它在参数表中的位置与形参匹配起来的。
在调用函数的时候,若形参个数和实参个数不同时,Lua 会自动调整实参个数。调整规则:若实参个数大于形参个数,从左向右,多余的实参被忽略;若实参个数小于形参个数,从左向右,没有被实参初始化的形参会被初始化为 nil。
local function fun1(a, b) --两个形参,多余的实参被忽略掉
print(a, b)
end
local function fun2(a, b, c, d) --四个形参,没有被实参初始化的形参,用nil初始化
print(a, b, c, d)
end
local x = 1
local y = 2
local z = 3
fun1(x, y, z) -- z被函数fun1忽略掉了,参数变成 x, y
fun2(x, y, z) -- 后面自动加上一个nil,参数变成 x, y, z, nil
-->output
1 2
1 2 3 nil
2.2 变长参数
Lua 还支持变长参数。若形参为 … , 表示该函数可以接收不同长度的参数。访问参数的时候也要使用 …。
local function func( ... ) -- 形参为 ... ,表示函数采用变长参数
local temp = {...} -- 访问的时候也要使用 ...
local ans = table.concat(temp, " ") -- 使用 table.concat 库函数对数
-- 组内容使用 " " 拼接成字符串。
print(ans)
end
func(1, 2) -- 传递了两个参数
func(1, 2, 3, 4) -- 传递了四个参数
-->output
1 2
1 2 3 4
注意:
LuaJIT 2 尚不能 JIT 编译这种变长参数的用法,只能解释执行。所以对性能敏感的代码,应当避免使用此种形式。
2.3 具名参数
Lua 还支持通过名称来指定实参,这时候要把所有的实参组织到一个 table 中,并将这个 table 作为唯一的实参传给函数。
local function change(arg) -- change 函数,改变长方形的长和宽,使其各增长一倍
arg.width = arg.width * 2
arg.height = arg.height * 2
return arg
end
local rectangle = { width = 20, height = 15 }
print("before change:", "width =", rectangle.width,
"height =", rectangle.height)
rectangle = change(rectangle)
print("after change:", "width =", rectangle.width,
"height =", rectangle.height)
-->output
before change: width = 20 height = 15
after change: width = 40 height = 30
2.4 按引用传递
当函数参数是 table 类型时,传递进来的是 实际参数的引用,此时在函数内部对该 table 所做的修改,会直接对调用者所传递的实际参数生效,而无需自己返回结果和让调用者进行赋值。
function change(arg) --change函数,改变长方形的长和宽,使其各增长一倍
arg.width = arg.width * 2 --表arg不是表rectangle的拷贝,他们是同一个表
arg.height = arg.height * 2
end -- 没有return语句了
local rectangle = { width = 20, height = 15 }
print("before change:", "width = ", rectangle.width,
" height = ", rectangle.height)
change(rectangle)
print("after change:", "width = ", rectangle.width,
" height =", rectangle.height)
--> output
before change: width = 20 height = 15
after change: width = 40 height = 30
注意:
在常用基本类型中,除了 table 是按址传递类型外,其它的都是按值传递参数。 用全局变量来代替函数参数的不好编程习惯应该被抵制,良好的编程习惯应该是减少全局变量的使用。
三、 函数返回值
3.1 返回值表示
Lua 具有一项与众不同的特性,允许函数返回多个值。在返回多个值时,值之间用“,”隔开。
local function swap(a, b) -- 定义函数 swap,实现两个变量交换值
return b, a -- 按相反顺序返回变量的值
end
local x = 1
local y = 20
x, y = swap(x, y) -- 调用 swap 函数
print(x, y) --> output 20 1
3.2 返回值调整
当函数返回值的个数和接收返回值的变量的个数不一致时,Lua 也会自动调整参数个数。
调整规则: 若返回值个数大于接收变量的个数,多余的返回值会被忽略掉; 若返回值个数小于参数个数,从左向右,没有被返回值初始化的变量会被初始化为 nil。
function init() --init 函数 返回两个值 1 和 "lua"
return 1, "lua"
end
x = init()
print(x)
x, y, z = init()
print(x, y, z)
--output
1
1 lua nil
3.3 函数调用多返回值函数
当一个函数有一个以上返回值,且函数调用不是一个列表表达式的最后一个元素,那么函数调用只会产生一个返回值, 也就是第一个返回值。
local function init() -- init 函数 返回两个值 1 和 "lua"
return 1, "lua"
end
local x, y, z = init(), 2 -- init 函数的位置不在最后,此时只返回 1
print(x, y, z) -->output 1 2 nil
local a, b, c = 2, init() -- init 函数的位置在最后,此时返回 1 和 "lua"
print(a, b, c) -->output 2 1 lua
当函数调用的实参列表也是一个列表表达式。
local function init()
return 1, "lua"
end
print(init(), 2) -->output 1 2
print(2, init()) -->output 2 1 lua
如果你确保只取函数返回值的第一个值,可以使用括号运算符
local function init()
return 1, "lua"
end
print((init()), 2) -->output 1 2
print(2, (init())) -->output 2 1
**注意: **
如果实参列表中某个函数会返回多个值,同时调用者又没有显式地使用括号运算符来筛选和过滤,则这样的表达式是不能被 LuaJIT 2 所 JIT 编译的,而只能被解释执行。
四、 全动态函数调用
调用回调函数,并把一个数组参数作为回调函数的参数。
local args = {...} or {}
method_name(unpack(args, 1, table.maxn(args)))
如果你的实参 table 中确定没有 nil 空洞,则可以简化为
local args = {...} or {}
method_name(unpack(args))
栗子:
local function run(x, y)
print('run', x, y)
end
local function attack(targetId)
print('targetId', targetId)
end
local function do_action(method, ...)
local args = {...} or {}
method(unpack(args, 1, table.maxn(args)))
end
do_action(run, 1, 2) -- output: run 1 2
do_action(attack, 1111) -- output: targetId 1111
**注意: **
unpack 内建函数还不能为 LuaJIT 所 JIT 编译,因此这种用法总是会被解释执行。对性能敏感的代码路径应避免这种用法。
五、 调用代码前先定义函数
Lua 里面的函数必须放在调用的代码之前,为什么放在调用后面就找不到呢?原因是 Lua 里的 function 定义本质上是变量赋值,即:
function foo() ... end
等价于
foo = function () ... end
因此在函数定义之前使用函数相当于在变量赋值之前使用变量,Lua 世界对于没有赋值的变量,默认都是 nil,所以也就产生了一个 nil 的错误。
一般地,由于全局变量是每个请求的生命期,因此以此种方式定义的函数的生命期也是每个请求的。为了避免每个请求创建和销毁 Lua closure 的开销,建议将函数的定义都放置在自己的 Lua module 中,例如:
-- my_module.lua
local _M = {_VERSION = "0.1"}
function _M.foo()
-- your code
print("i'm foo")
end
return _M
然后,再在 content_by_lua_file 指向的 .lua 文件中调用它:
local my_module = require "my_module"
my_module.foo()
因为 Lua module 只会在第一次请求时加载一次(除非显式禁用了 lua_code_cache 配置指令),后续请求便可直接复用。