Lua语言中函数本质上是一个function类型的值。这个值可以作为表达式的一部分,也可以被赋予变量。当被赋予变量时,变量名则为函数名。

由于函数是一个值,因此Lua中函数都是匿名的,所谓的函数名则是保存函数值的变量

闭包指的是一个函数和该函数能够访问其外部作用域的局部变量的机制

函数和闭包的区别在于,函数是没有访问外部作用域的局部变量的闭包

函数定义

在Lua中函数是对语句和表达式进行抽象的主要方式。

函数既可以被视为语句也可以被视为表达式。需要使用到函数返回值的一般会被视为表达式,否则被视为语句。

print(var) -- 语句
a = math.sin(3) + math.cos(10) -- 表达式

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

print "Hello, world" -- 仅一个字符串作为参数
f{x=10, y=20} -- 表构造器

Lua程序既可以调用Lua语言编写的函数,也可以调用C语言编写的函数。一般情况下,可以用C语言编写对性能要求较高,或不容易直接通过Lua语言进行操作的操作系统机制等。

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

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

参数与返回值

返回值

Lua语言允许函数返回多个结果。Lua语言中几个预定义的函数就会返回多个值,比如string.find函数找到对应的模式之后会返回起始字符和结尾字符的索引值(字符串中的第一个字符的索引值为1)。

Lua语言编写的函数返回多个结果,只需要在return关键字后列出所有要返回的值即可

Lua语言根据函数的被调用情况调整返回值的数量

  1. 当函数被作为一条单独语句调用时,所有返回值都会被抛弃;
  2. 当函数被作为表达式(例如,加法的操作数)调用时,将保留函数的第一个返回值。
  3. 只有当函数是一系列表达式中的最后一个表达式(或是唯一一个表达式)时,其所有的返回值才能被捕获到。

这里的一系列表达式主要表现为:多重赋值函数调用时传入的实参列表表构造器return语句。

这里给出几个函数示例,下面示例中会用到

function foo0() end

function foo1() return "a" end

function foo2() return "a", "b" end

多重赋值

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

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()        -- 返回值不够,用nil补齐
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
x, y = foo0(), 20, 30 -- 不是唯一或者最后一个表达式,返回一个值
print(x, y)           -- nil 20

函数调用时传入的实参列表

当一个函数调用是另一个函数调用的最后一个(或者是唯一)实参时,第一个函数的所有返回值都会被作为实参传给第二个函数。

print(foo0())        -- 无返回值
print(foo1())        -- a
print(foo2())        -- a  b
print(foo2(), 1)     -- a   1 不是最后一个或唯一一个实参
print(foo2() .. "x") -- ax

当调用f(g())时,如果f的参数是固定的,那么Lua语言会将g的返回值的个数调整与f的参数个数一致。满足多重赋值的逻辑。

表构造器

表构造器会完整的接收函数调用的所有返回值,而不会调整返回值的个数。前提是函数调用是表达式列表的最后一个

t = {foo2()}  -- t = {"a", "b"}
t = {foo0(), foo2(), 4} -- t[1] = nil, t[2] = "a", t[3] = 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

可变长参数

Lua语言中的函数可以是可变长参数函数,即可以支持数量可变的参数。

function add(...)
    local s = 0
    for _, v in ipairs({ ... }) do
        s = s + v
    end
    return s
end

print(add(3, 7, 10, 25, 12)) -- 57
print(add(3, 7, 10)) -- 20

如上例所示,参数列表中的三个点(...)表示该函数的参数是可变长的。当函数需要访问这些参数时仍需要用到三个点,此时这三个点是作为表达式来使用的。在这个例子中,表达式{...}的结果是一个由所有可变长参数组成的列表,该函数会遍历该列表来累加其中的元素。

Lua中把三个点组成的表达式称为可变长参数表达式,其行为类似于一个具有多个返回值的函数,返回的是当前函数的所有可变长参数。

可变长参数的一些用法

local a, b = ... -- 创建两个局部变量,其值为前两个可选的参数,参数不存在则为nil

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

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

具有可变长参数的函数也可以具有任意数量的固定参数,但固定参数必须处于变长参数之前。Lua语言会先将前面的参数赋给固定参数,然后将剩余的参数作为可变长参数。

要遍历可变长参数,函数可以使用表达式{...}将可变长参数放在一个表中。

如果可变长参数中存在nil,那么{...}获得的表可能不是一个有效的序列(所有元素都不为nil的数组),此时就无法判断原始参数是否以nil结尾,遍历时可能到中途就会终止

函数table.pack

函数table.pack(...)像表达式{...}一样保存所有的参数,然后将其放在一个表中返回,但是这个表还有一个保存了参数个数的额外字段"n",形如local args = { 1, 2, 3, 4, n = 4 }

下面的函数使用函数table.pack()函数检测参数中是否有nil值:

function nonils(...)
    local args = table.pack(...)
    for i = 1, args.n do
        if args[i] == nil then
            return false
        end
    end
    return true
end

function add(...)
    if not nonils(...) then
        return 0
    end
    local s = 0
    for _, v in ipairs({ ... }) do
        s = s + v
    end
    return s
end

print(add(3, 7, 10, 25, 12)) -- 57
print(add(3, 7, 10)) -- 20

print(add(3, 7, nil, 25, 12)) -- 0

函数select

函数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

函数select的一种用法是通过参数n获取第n个可变参数,使用方法是只用一个变量来接受返回值,接收到的返回值就是第n个额外参数

下面是一个例子:

local function add(...)
    local s = 0
    for i = 1, select("#", ...) do
        s = s + select(i, ...) -- 获取第i个可变参数,这个表达式中函数select只会返回一个值
    end
	return s
end

该函数的缺点在于多次带有多个参数调用select函数会增加开销,比直接for in ipairs{...}创建表的开销更大。

函数table.unpack

table.pack函数相反,table.unpack函数的参数是一个数组,返回值是数组内的所有元素。可用于将数组转为可变长参数,作为需要可变长参数的函数的参数。

函数table.unpack函数使用长度操作符#来获取返回值的个数,因此该函数只能用于序列。

可以通过传入第2、3个参数限制返回元素的范围。

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