lua快速入门 —— 函数

本文最后更新于:2020年1月27日 晚上

概览:Lua的函数的基本形式、如何调用以及函数的多重返回值、变长参数、具名实参。此外还有Lua中函数的地位,即函数是类似于数字那样的第一类值,还有匿名函数、闭包、局部函数以及尾调用函数。

参照书籍:《Lua程序设计(第二版)》

Lua版本:Lua 5.3.5

总结

Lua中的函数是第一类值,可以像变量那样被赋给其他的变量、被存储到表中、作为函数参数或者是函数的返回值,即实际上函数就像一个变量一样,函数是“匿名的”,函数名只不过是指向了那个函数而已。Lua中的函数可以有多个返回值,Lua可以接受非固定数量的参数,同时调用Lua函数时,可以指定参数以实参。此外Lua支持匿名函数,闭包,Lua中的函数尾调用类似于goto语句,可以避免栈操作。


在Lua中函数是一种对语句或者表达式进行抽象的主要机制。

函数的基本形式

1
2
3
4
-- 函数实现语法规则:
function func_name(argument-list) -- 函数名(参数)
statements-list; -- 函数体
end
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
-- 实例
function func_name()
print "hello" -- 见下面的说明
end

func_name() -- 函数的调用

-- 计算从1加到n的和
function adds(n)
local sum = 0
for i = 0,n do
sum = sum + i
end
return sum
end

print(adds(4)) -- 10

函数的调用

同其他语言类似,函数名+(参数) 若参数列表为空也需要加上括号()。

例外:当函数有且只有一个参数,并且该参数是字符串或者表构造式时,()可有可无。

1
2
3
4
5
6
7
8
9
10
11
12
print "Hello World"      <-->      print("Hello World")

dofile 'a.lua' <--> dofile ('a.lua')

print [[a multi-line <--> print([[a multi-line

message]] message]])

f{x=10, y=20} <--> f({x=10, y=20})

type{} <--> type({})

以及面向对象式的调用函数:

1
o:foo(x)  <--> o.foo(o,x)

此外,Lua程序既可以使用以Lua编写的程序,也可以调用C语言或者宿主程序使用的其他语言编写的程序

函数的参数

若实参多于形参,则舍弃多余的实参,若实参不足,则多余的形参初始化为nil。

1
2
3
4
5
6
7
8
9
function f(a, b) return a or b end

CALL PARAMETERS

f(3) a=3, b=nil

f(3, 4) a=3, b=4

f(3, 4, 5) a=3, b=4 (5 is discarded)

用处:用于默认参数的应用。

1
2
3
4
function incCount(n)
n = n or 1
count = count + n
end

上述函数使用1作为默认参数,若直接调用IncCount()函数而不传递参数时,count值增加1。因为不传递参数则n被初始化为nil,而 nil or 1 返回值为1,故最终初始化为1.

多重返回值

Lua与众不同的特征:允许函数返回多个结果

1
2
3
--例如 string.find() 返回匹配串的开始和结束的下标,如果不存在返回nil
s,e = string.find("hello Lua users","Lua")
print(s,e) --> 7 9
1
2
3
4
5
6
7
8
9
10
11
12
13
14
-- 取得一个table中最大的值和该值最大的下标
function maximum(a)
local mi = 1 -- max value index
local m = a[mi] -- max value
for i,val in ipairs(a) do
if val > m then
mi = i
m = val
end
end
return m,mi
end

print(maximum({23,56,48,10})) --> 56 2

Lua的多重返回值使用

Lua会调整一个函数的返回值来适应不同的情况。

  1. 若函数作为单独的语句时,Lua会丢弃函数的返回值。
  2. 若函数作为表达式的一部分来调用时,Lua只会保留函数的第一个返回值。
  3. 若函数调用是一系列表达式中的最后一个元素时,此时才能获得所有的返回值。
  4. 当函数调用作为另一个函数调用的最后一个实参时,这个函数的全部返回值都会作为实参传递给第二个参数,比如print函数
  5. 若函数调用存在于table构造式之中时,且该函数调用是作为最后一个元素时,获取全部结果,否则其他位置只产生一个结果。
  6. return语句,例如return maximum({23,56,48,10})将返回函数的全部返回值。
  7. 将函数调用单独放在一对圆括号之中,将会只返回一个结果,所以return语句也不要随意加括号。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
x,y = maximum({23,56,48,10}) --> 56  2

x = maximum({23,56,48,10}) --> 56

-- 情况3 获取所有的返回值
x,y,z = 100,maximum({23,56,48,10}) -->100 56 2

-- 情况2 只产生一个值
x,y = maximum({23,56,48,10}),100 --> 56 100

-- 情况4
print(maximum({23,56,48,10})) --> 56 2

-- 情况2
print(maximum({23,56,48,10}).."hello") -->56hello

-- 情况5
t = {maximum({23,56,48,10})}
print(t[1]) --> 56
print(t[2]) --> 2

t1 = {maximum({23,56,48,10}),100}
print(t1[1]) --> 56
print(t1[2]) --> 100

-- 情况6
function a()
return maximum({23,56,48,10})
end

print(a()) --> 56 2

-- 情况7
print((a())) --> 56

特殊函数 unpack

unpack接收一个数组作为参数,并从下标1开始返回该数组的所有元素。

1
2
3
4
5
print(unpack({1,2,3}))  --> 1  2  3
print(unpack{1,2,3}) --> 1 2 3
-- 当函数有且只有一个参数,并且该参数是字符串或者表构造式时,()可有可无。

x,y = unpack{1,2,3} --> 1 2

unpack的一项重要用途体现在泛型调用机制中。泛型调用机制可以动态地以任何实参来调用任何函数。

举例来说ANSI C中无法编写泛型调用的代码。最多是声明一个能接受变长参数的函数(stdarg.h),或者使用一个函数指针来调用不同的函数。并且在C语言中,无法在同一函数调用中传入动态数量的参数,也就是说,在每次调用不同的函数时必须传入固定数量的参数,并且每个参数都具有确定的类型。

在Lua中,如果你想调用任意函数f,而所有的参数都在数组a中,可以:f(unpack(a))

1
2
3
f = string.find
a = {"hello", "ll"}
print(f(unpack(a))) --> 3 4

预定义的unpack函数是用C语言实现的,我们也可以用Lua来完成:

1
2
3
4
5
6
function unpack(t, i)
i = i or 1
if t[i] then
return t[i], unpack(t, i + 1)
end
end

变长参数

在函数的实现中,括号里的参数列表使用三个点 (…) 来表示该函数可以接受不同数量的实参。

而当函数内要调用变长参数时,仍然需要直接使用三个点 (…) 。

1
2
3
4
5
6
7
8
9
10
11
-- 计算所有参数的总和
function add(...)
local sum = 0
print(...) --直接使用可变参数
for i,v in ipairs{...} do
sum = sum + v
end
return sum
end

print(add(1,2,3,4,5)) -->15
  • 若函数还有固定参数,固定参数必须放在可变参数之前。
  • 若函数还有固定参数,则实际传参时优先匹配固定参数,多余参数全部被赋予可变参数
  • 通常一个函数在遍历其变长参数时只需要使用表达式...,然而在某些特殊情况下,变长参数可能会包含一些故意传入的 nil ,那么此时需要用函数select来访问变长参数了。
  • ... 的操作是Lua5.1之后的版本支持的,Lua5.0版本提供了隐含变量arg来接受所有的变长参数。 ——《Lua程序设计第二版 P41》

特殊情况时使用select访问变长参数

调用select函数需要传入一个固定实参selector(选择开关)和一系列变长参数。

  • 若selector为数字n,那么select函数就会返回它的第n个可变实参(实际上,下面的代码测试为从第i个开始的参数,select(1,…) 实际返回的是多个返回值)
  • 否则selector只能为字符串”#”,这样select会返回变长参数的总数。
  • select("#", ...) 会返回所有变长参数的总数,其中包括nil。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
-- 计算所有参数的总和
function add(...)
local sum = 0

for i=1,select("#", ...) do
print(select(i,...))
end

for i,v in ipairs{...} do
sum = sum + v
end
return sum
end

print(add(1,2,3,4,5)) -->15

具名实参

顾名思义,就是使用函数时,通过指定形参的名称来给形参赋值。

但是Lua不能像其他语言那样直接使用类似rename(old="temp.lua", new="temp1.lua")这样的方式,Lua可以通过将所有的参数放在一个表中,把表作为函数的唯一参数来实现。

1
rename{old="temp.lua", new="temp1.lua"}

然后根据这个想法可以重新定义rename

1
2
3
function rename (arg)
return os.rename(arg.old, arg.new)
end

当函数的参数很多的时候,这种函数参数的传递方式很方便的。例如GUI库中创建窗体的函数有很多参数并且大部分参数是可选的,可以用下面这种方式:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
w = Window {
x=0, y=0, width=300, height=200,
title = "Lua", background="blue",
border = true
}
function Window (options)
-- check mandatory options
if type(options.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
-- everything else is optional
_Window(options.title,
options.x or 0, -- default value
options.y or 0, -- default value
options.width, options.height,
options.background or "white", -- default
options.border -- default is false (nil)
)
end

进阶:函数是第一类值

Lua中的函数是带有词法定界(lexical scoping)的第一类值(first-class values)。

第一类值指:在Lua中函数和其他值(数值、字符串)一样,函数可以被存放在变量中,也可以存放在表中,可以作为函数的参数,还可以作为函数的返回值。

词法定界指:嵌套的函数可以访问他外部函数中的变量。这一特性给Lua提供了强大的编程能力。

在Lua中,”函数和所有其它的值一样都是匿名的“,当我们在说某个函数时,比如print,实际上是在讨论拥有了一个函数的变量而已,print也可以被赋值。

1
2
3
4
5
6
7
8
9
10
11
b = print
b("123")

a = {p = print}
a.p("Hello World") --> Hello World

print = math.sin -- `print'被赋值为正弦函数
a.p(print(1)) --> 0.841470

sin = a.p -- `sin' now refers to the print function
sin(10, 20) --> 10 20

所以函数就像一个变量一样,是可以由表达式创建的,我们在本篇博客开头看见的函数创建方式实际类似于“语法糖”。

1
2
3
-- function foo(x) return 2*x end
foo = function (x) return 2*x end -- 创建一个变量,赋值为函数
print(foo(1)) --> 2

这样创建的函数实际就像是创建了一个匿名函数,然后再将匿名函数赋予了一个变量。

由于Lua是第一类值,所以不仅可以将其存储在全局变量中,还可以存储在局部变量中甚至table的字段中。

函数可以存储到table字段中

有部分Lua库将函数存储在table字段中,例如:io.read,math.sin。

复习前面接触到的table的语法糖:a = {} a.x就是a[“x”]。即以字符串x为索引。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
-- 方式一
Lib = {}
Lib.foo = function (x,y) return x+y end
Lib.goo = function (x,y) return x-y end

print(Lib.foo(2,3)) --> 5

-- 方式二
Lib = {
foo = function (x,y) return x+y end
goo = function (x,y) return x-y end
}

-- 方式三
Lib = {}
function Lib.foo (x,y) return x+y end
function Lib.goo (x,y) return x+y end

匿名函数的应用

table标准库提供一个排序函数,接受一个表作为输入参数并且排序表中的元素。这个函数必须能够对不同类型的值(字符串或者数值)按升序或者降序进行排序。Lua不是尽可能多地提供参数来满足这些情况的需要,而是接受一个排序函数作为参数(类似C++的函数对象),排序函数接受两个排序元素作为输入参数,并且返回两者的大小关系,例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
network = {
{name = "grauna", IP = "210.26.30.34"},
{name = "arraial", IP = "210.26.30.23"},
{name = "lua", IP = "210.26.23.12"},
{name = "derain", IP = "210.26.23.20"},
}

table.sort(network, function (a,b) return (a.name > b.name) end)

for i,v in ipairs(network) do
print(v.name , v.IP)
end

--[[
lua 210.26.23.12
grauna 210.26.30.34
derain 210.26.23.20
arraial 210.26.30.23
--]]

闭包 closure

词法域:若将一个函数写在另一个函数内,那么这个位于内部的函数便可以访问外部函数中的局部变量,这项特征称之为“词法域”。

closure简单的讲,一个closure就是一个函数加上该函数所需访问的所有的“非局部的变量”。

非局部的变量

1
2
3
4
5
6
7
8
9
10
11
12
name = {"Peter","Paul","Mary"}
grades = {Peter = 8,Paul = 7,Mary = 10}
table.sort(name, function (n1,n2)
return grades[n1] > grades[n2] -- 比较年级
end)

-- 改写代码,封装函数
function sortbygrade(names,grades)
table.sort(names,function(n1,n2)
return grades[n1] > grades[n2]
end)
end

对于sortbygrade()函数来说,传递给sort的匿名函数可以去访问参数grades,而grades是外部函数sortbygrade()局部变量。但对于匿名函数来说,grades既不是全局变量,也不是局部变量,而是将其称为一个非局部的变量(non-local variable 或 upvalue)。

closure

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
function newCounter()
local i = 0
return function()
i = i + 1
return i
end
end

c1 = newCounter()

print(c1()) --》 1
print(c1()) --》 2

c2 = newCounter()
print(c2()) --》 1

在这里c1和c2是同一个函数创建的两个不同的closure,他们各自之间拥有局部变量i的独立实例。

闭包的内容对应于《Lua程序设计第二版 P47-50》

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
-- 计算y = a*x + b

--普通函数写法
function func(a,b,x)
return a*x+b
end

--闭包的写法
function func1(a,b)
return function(x)
return a*x+b
end
end

--这样对于计算ab固定的一阶函数的时候,比较方便
y1 = func1(2,3)

ends = y1(5) --> 13
ends = y1(4) --> 11

from Colourso>

Lua的闭包和Python的闭包有些相似,上面的这个例子就是我接触Python闭包时的例子,Python闭包网址: http://www.colourso.top/Python/

closure的用途举例:创建一个安全环境

把原始版本的函数放在一个局部变量里,然后这个函数被访问的唯一方法就是通过新版本的函数。

例如:当我们运行一段不信任的代码(比如我们运行网络服务器上获取的代码)时安全的环境是需要的,比如我们可以使用闭包重定义io库的open函数来限制程序打开的文件。

1
2
3
4
5
6
7
8
9
10
do
local oldOpen = io.open
io.open = function (filename, mode)
if access_OK(filename, mode) then
return oldOpen(filename, mode)
else
return nil, "access denied"
end
end
end

非全局的函数

将一个函数存储到一个局部变量中,即得到了一个“局部函数”,这个函数只能在某个特定的作用域中使用。

1
2
3
4
5
6
local f = function(x) return x end

--Lua的语法糖
local function f(x)
return x
end

局部递归函数注意事项

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
-- 错误
local fact = function (n)
if n == 0 then
return 1
else
return n*fact(n-1) -- 错误
end
end

-- 上面这种方式导致Lua编译时遇到fact(n-1)并不知道他是局部函数fact,Lua会去查找是否有这样的全局函数fact。为了解决这个问题我们必须在定义函数以前先声明:

local fact
fact = function (n)
if n == 0 then
return 1
else
return n*fact(n-1)
end
end
-- 这样在fact内部fact(n-1)调用是一个局部函数调用,运行时fact就可以获取正确的值了

尾调用

当函数最后一个动作是调用另外一个函数时,我们称这种调用尾调用。例如:

1
2
3
function f(x)
return g(x) -- g的调用就是尾调用
end

上面例子中f调用g后不会再做任何事情,这种情况下当被调用函数g结束时程序不需要返回到调用者f;所以尾调用之后程序不需要在栈中保留关于调用者的任何信息。一些编译器比如Lua解释器利用这种特性在处理尾调用时不使用额外的栈,我们称这种语言支持正确的尾调用。

由于尾调用不需要使用栈空间,那么尾调用递归的层次可以无限制的

1
2
3
4
-- 这个函数无论n为何值都不会导致栈溢出
function foo(n)
if n > 0 then return foo(n-1) end
end

正确的尾调用

形如:return func(args) 这样的调用形式才是真正的尾调用。

1
2
3
4
5
return x[i].foo(x[j] + a*b,i+j) --正确的尾调用
-- 以下都不是正确的尾调用,因为执行完g(x)后还需要额外做运算
return g(x)+1
return x or g(x)
return (g(x)) -- ()作用是把返回结果调整为一个值

尾调用的应用 —— 编写状态机(state machine)

状态机的应用要求函数记住每一个状态,改变状态只需要goto(or call)一个特定的函数。我们考虑一个迷宫游戏作为例子:迷宫有很多个房间,每个房间有东西南北四个门,每一步输入一个移动的方向,如果该方向存在即到达该方向对应的房间,否则程序打印警告信息。目标是:从开始的房间到达目的房间。

这个迷宫游戏是典型的状态机,每个当前的房间是一个状态。我们可以对每个房间写一个函数实现这个迷宫游戏,我们使用尾调用从一个房间移动到另外一个房间。一个四个房间的迷宫代码如下:

我们可以调用room1()开始这个游戏。

如果没有正确的尾调用,每次移动都要创建一个栈,多次移动后可能导致栈溢出。但正确的尾调用可以无限制的尾调用,因为每次尾调用只是一个goto到另外一个函数并不是传统的函数调用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
function room1 ()
local move = io.read()
if move == "south" then
return room3()
elseif move == "east" then
return room2()
else
print("invalid move")
return room1() -- stay in the same room
end
end

function room2 ()
local move = io.read()
if move == "south" then
return room4()
elseif move == "west" then
return room1()
else
print("invalid move")
return room2()
end
end

function room3 ()
local move = io.read()
if move == "north" then
return room1()
elseif move == "east" then
return room4()
else
print("invalid move")
return room3()
end
end

function room4 ()
print("congratilations!")
end