本文最后更新于: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() function adds (n) local sum = 0 for i = 0 ,n do sum = sum + i end return sumend print (adds(4 ))
函数的调用 同其他语言类似,函数名+(参数)
若参数列表为空也需要加上括号()。
例外:当函数有且只有一个参数,并且该参数是字符串 或者表构造式 时,()可有可无。
1 2 3 4 5 6 7 8 9 10 11 12 print "Hello World" <dofile 'a.lua' <print [[a multi-line <--> print([[a multi-line message]] message]] ) f{x=10 , y=20 } <type {} <
以及面向对象式的调用函数:
此外,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 + nend
上述函数使用1作为默认参数,若直接调用IncCount()函数而不传递参数时,count值增加1。因为不传递参数则n被初始化为nil,而 nil or 1
返回值为1,故最终初始化为1.
多重返回值 Lua与众不同的特征:允许函数返回多个结果 。
1 2 3 s,e = string .find ("hello Lua users" ,"Lua" )print (s,e)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 function maximum (a) local mi = 1 local m = a[mi] for i,val in ipairs (a) do if val > m then mi = i m = val end end return m,miend print (maximum({23 ,56 ,48 ,10 }))
Lua的多重返回值使用 Lua会调整一个函数的返回值来适应不同的情况。
若函数作为单独的语句时,Lua会丢弃函数的返回值。
若函数作为表达式的一部分来调用时,Lua只会保留函数的第一个返回值。
若函数调用是一系列表达式中的最后一个元素时,此时才能获得所有的返回值。
当函数调用作为另一个函数调用的最后一个实参时,这个函数的全部返回值都会作为实参传递给第二个参数,比如print函数
若函数调用存在于table构造式之中时,且该函数调用是作为最后一个元素时,获取全部结果,否则其他位置只产生一个结果。
return语句,例如return maximum({23,56,48,10})
将返回函数的全部返回值。
将函数调用单独放在一对圆括号之中,将会只返回一个结果,所以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 }) x = maximum({23 ,56 ,48 ,10 }) x,y,z = 100 ,maximum({23 ,56 ,48 ,10 }) x,y = maximum({23 ,56 ,48 ,10 }),100 print (maximum({23 ,56 ,48 ,10 })) print (maximum({23 ,56 ,48 ,10 }).."hello" ) t = {maximum({23 ,56 ,48 ,10 })}print (t[1 ]) print (t[2 ]) t1 = {maximum({23 ,56 ,48 ,10 }),100 }print (t1[1 ]) print (t1[2 ]) function a () return maximum({23 ,56 ,48 ,10 })end print (a()) print ((a()))
特殊函数 unpack unpack接收一个数组作为参数,并从下标1开始返回该数组的所有元素。
1 2 3 4 5 print (unpack ({1 ,2 ,3 })) print (unpack {1 ,2 ,3 }) x,y = unpack {1 ,2 ,3 }
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)))
预定义的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 sumend print (add(1 ,2 ,3 ,4 ,5 ))
若函数还有固定参数,固定参数必须放在可变参数之前。
若函数还有固定参数,则实际传参时优先匹配固定参数,多余参数全部被赋予可变参数
通常一个函数在遍历其变长参数时只需要使用表达式...
,然而在某些特殊情况下,变长参数可能会包含一些故意传入的 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 sumend print (add(1 ,2 ,3 ,4 ,5 ))
具名实参 顾名思义,就是使用函数时,通过指定形参的名称来给形参赋值。
但是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) 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 _Window(options.title, options.x or 0 , options.y or 0 , options.width, options.height, options.background or "white" , options.border )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" ) print = math .sin a.p(print (1 )) sin = a.p sin (10 , 20 )
所以函数就像一个变量一样,是可以由表达式创建的,我们在本篇博客开头看见的函数创建方式实际类似于“语法糖”。
1 2 3 foo = function (x) return 2 *x end print (foo(1 ))
这样创建的函数实际就像是创建了一个匿名函数,然后再将匿名函数赋予了一个变量。
由于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 )) 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
闭包 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()) print (c1()) c2 = newCounter()print (c2())
在这里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 function func (a,b,x) return a*x+bend function func1 (a,b) return function (x) return a*x+b end end y1 = func1(2 ,3 ) ends = y1(5 ) ends = y1(4 )
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 local function f (x) return xend
局部递归函数注意事项 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 local fact fact = function (n) if n == 0 then return 1 else return n*fact(n-1 ) end end
尾调用 当函数最后一个动作是调用另外一个函数时,我们称这种调用尾调用。例如:
1 2 3 function f (x) return g(x) end
上面例子中f调用g后不会再做任何事情,这种情况下当被调用函数g结束时程序不需要返回到调用者f;所以尾调用之后程序不需要在栈中保留关于调用者的任何信息 。一些编译器比如Lua解释器利用这种特性在处理尾调用时不使用额外的栈 ,我们称这种语言支持正确的尾调用。
由于尾调用不需要使用栈空间,那么尾调用递归的层次可以无限制的 。
1 2 3 4 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) 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() 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