函数有两种用途:
- 完成指定任务,此时函数作为调用语句使用。
- 计算并返回值,此时函数作为赋值语句的表达式使用。
function fn(arguments-list)
statements-list
end
调用函数时,如果参数列表为空,必须使用()
表明是函数调用。
print(os.date())
当函数只有一个参数且参数是字符串或表构造式时,()
是可选的。
print "hello world"
dofile "main.lua"
print [[a multi-line message]]
fn{x=10, y=20}
type{}
在Lua中函数都是function
类型的对象
- 可被比较
- 可赋值给一个变量
- 可传递给函数
- 可从函数中返回
- 可作为table表中的键
函数定义
Lua使用关键字function
定义函数
function fn(arg)
-- function body...
end
函数定义的语法会定义一个全局函数,名为fn
,全局函数本质上是函数类型的值赋给全局变量。
函数变量式
fn = function(arg)
-- function body...
end
由于函数定义本质上是变量赋值,变量的定义总是应放置在变量使用之前,所以函数的定义也需要放置在函数调用之前。
local function fn(arg)
-- function body...
end
local fn = function(arg)
-- function body...
end
如果参数列表为空,必须使用()
表明函数调用。
定义函数并调用
-- 定义函数
function fn()
print("hello function")
end
-- 调用函数
fn()
在定义函数时要注意
- 利用名字来解释函数,变量的目的使人通过名字就能看出函数、变量的作用。
- 每个函数的长度要尽量控制在一个屏幕内,一样就能看明白。
- 让代码自己说话,最好是不需要注释。
由于函数定义等价于变量赋值,因此也可以将函数名替换为Lua表中的某个字段。
-- 这种形式的函数定义不能使用local修饰符,因为不存在定义新的局部变量了。
foo.bar = function()
-- function body...
end
function foo.bar()
-- function body ...
end
案例:接收两个参数,计算加减乘除的结果,并输出到屏幕。
function fn(i,j)
return i+j, i-j, i*j, i/j, i%j;
end
a,b,c,d,e = fn(10,5)
print(a,b,c,d,e) -- 15 5 50 2.0 0
函数参数
按值传递
Lua函数的参数大部分是按值传递的,值传递就是调用函数时,实参把它的值通过赋值运算传递给形参,然后形参的改变和实参就没有关系了。在这个过程中,实参是通过它在参数表中的位置与形参匹配起来的。
local function swap(x,y)
local tmp = x
x = y
y = tmp
print(x,y)
end
在调用函数时,若形参个数和实参个数不同时,Lua会自动调整实参个数。调整规则:
若实参个数大于形参个数,从左向右,多余的实参被忽略。若实参个数小于形参个数,从左向右,没有实参初始化的形参会被初始化为nil
。
变长参数
https://moonbingbing.gitbooks.io/openresty-best-practices/content/lua/function_parameter.html
若定义一个函数,参数个数不固定,应该怎么办呢?这就涉及到Lua中函数的可变参数。
-- Lua中三个点表示函数的参数个数不确定,可以改变,即可变参数。
function fn(...)
end
return
关键字只能出现在语句块的结尾,也就是说,在end
之前,或者是else
之前,或者是until
之前。
function fn(x)
return x*x*x;
end
n = fn(5)
print(n) -- 125
function fn(x)
if x<10 then
return x*x*x
else
return x*x
end
end
n = fn(5)
print(n)-- 125
函数基础
Lua中函数是一种对语句和表达式进行抽象的主要机制。
Lua中函数即可完成某项特定的任务,一条函数调用可视为一条语句。
$ lua
> a = match.sin(3) + math.cos(10)
> print(a)
-0.69795152101659
> print(os.date())
02/04/18 17:42:52
Lua函数也可以只做一些计算并返回结果,可视为一句表达式。
$ lua
> print(8*9, 9/8)
72 1.125
无论哪种用法都需将参数放入一对圆括号中。即使调用函数时没有参数,也必须写出一对空括号。对此规则只有一种特殊的例外情况:一个函数若只有一个参数,并且此参数是一个字面量字符串或table构造式,那么圆括号便是可有可无的。
$ lua
> print "hello world"
> dofile "test.lua"
> print [[a multi-line message]]
函数只有一个参数,且参数是一个table构造式,则圆括号可有可无。
> f{x=10, y=20}
> f({x=10, y=20})
> type{}
> type({})
Lua为面向对象式的调用也提供了一种特殊的语法 - 冒号操作符。
> obj.foo(obj, arg)
-- 冒号操作符使调用obj.foo时将obj隐含地作为函数的第一个函数
> obj:foo(arg)
Lua程序即可使用以Lua编写的函数,也可调用以C语言编写的函数。
function add(params)
local sum=0;
for k,v in ipairs(params) do
sum=sum+v
end
return sum
end
print(add({1,2,3}))
在这种语法中,一个函数定义具有一个名词(函数名)、一些列参数(参数表)、一个函数体(一系列语句)。
形式参数(parameter)的使用方式与局部变量非常类似,它们是由调用函数时的实际参数(argument)初始化的。
调用函数时提供的实参数量可与形参数量不同,Lua会自动调整实参数量,以匹配参数表的要求。这项调整与多重赋值(multiple assignment)很相似,即“若实参多于形参,则舍弃多于实参;若实参不足,则多余形参初始化为nil”。
function fn(a,b)
print(a,b);
end
fn(1) -- 1 nil
fn(1,2) -- 1 2
fn(1,2,3) -- 1 2
虽然这种调整行为会导致一些编程错误,但它也是很有用的,尤其是对于默认实参的应用。
function incCount(n)
n = n or 1 -- 函数以1作为默认实参
count = count + n
print(n, count)
end
fn() -- attempt to perform arithmetic on a nil value (global 'count')
多重返回值
Lua函数具有一项非常与众不同的特征,允许函数返回多个结果。
Lua的几个预定义函数就是返回多个值的,string.find()
用于在字符串中定位一个模式(pattern),该函数允许在字符串中找到指定的模式,将返回匹配的起始字符和结尾字符的索引。
startstr,endstr = string.find("hello lua", "lua")
print(startstr, endstr) -- 7 9
以Lua编写的函数同样可以返回多个结果,只需在return
关键字后列出所有的返回值即可。
例如:查找数组中最大元素并返回该元素的位置
function max(tbl)
local index = 1
local value = tbl[index]
for i,v in ipairs(tbl) do
if(v > value) then
index = i
value = v
end
end
return index,value
end
print(max{9,2,12,8,3,9})-- 3 12
Lua会调整函数返回值数量以适应不同的调用情况
- 若将函数调用作为一条单独语句时,Lua会丢弃函数的所有返回值。
- 所将函数作为表达式的一部分来调用时,Lua只保留函数的第一个返回值。只有当一个函数调用是“一系列表达式”中最后一个元素或仅有一个元素时,才能获得它的所有返回值。
-- test
local tbl = {9,2,12,8,3,9}
local a = max(tbl)
print(a) -- 3
local a,b = max(tbl)
print(a, b) -- 3 12
local a,b,c = max(tbl)
print(a,b,c)-- 3 12 nil
local a,b,c = 1, max(tbl)
print(a,b,c)-- 1 3 12
local a,b,c,d = 1,max(tbl),0
print(a,b,c,d) -- 1 3 0 nil
这里所谓的“一系列表达式”,在Lua中表现为4种情况
- 多重赋值
- 函数调用时传入的实参列表
-
table
的构造式 -
return
语句
在多重赋值中,若函数调用是最后的或仅有的一个表达式,那么Lua会保留其尽可能多的返回值,用于匹配赋值变量。若函数没有返回值或没有足够多的返回值,那么Lua会用nil
来补充缺失的值。若函数调用不是一些列表达式的最后一个元素,那么将只产生一个值。
当函数调用作为另一个函数调用的最后一个或仅有的实参时,第一个函数所有返回值都将作为实参传入第二个函数。
function fn1()
end
function fn2()
return 1
end
function fn3()
return 1,2
end
print(fn1())
print(fn2()) -- 1
print(fn3()) -- 1 2
print(fn2(),2) -- 1 2
print(fn2()..'x') -- 1x
table
构造式可以完整地接收一个函数调用的所有结果,即不会有任何数量方面的调整。
function fn1()
end
function fn2()
return 1
end
function fn3()
return 1,2
end
tbl1 = {fn1()} -- 相当于 tbl1={}
tbl2 = {fn2()} -- 相当于 tbl2={1}
tbl3 = {fn3()} -- 相当于 tbl3={1,2}
不过这种行为只有当一个函数调用作为最后一个元素时才会发生,而在其他位置上的函数调用总是只缠身给一个结果值。
function fn1()
end
function fn2()
return 1
end
function fn3()
return 1,2
end
tbl = {fn1(), fn2(), fn3(), 4}
print(tbl[1], tbl[2], tbl[3], tbl[4]) -- nil 1 1 4
最后一种情况是return
语句,诸如return fn()
这样的语句将返回fn
的所有返回值。
function fn1()
end
function fn2()
return 1
end
function fn3()
return 1,2
end
function fn(i)
if i==1 then return fn1()
elseif i==2 then return fn2()
elseif i==3 then return fn3()
end
end
print(fn(1), fn(2), fn(3)) -- nil 1 1 2
-- 将函数调用放入一对圆括号中,从而迫使它只返回一个结果。
print((fn(1)), (fn(2)), (fn(3))) -- nil 1 2
注意return
语句后面的内容不需要圆括号,在该位置上书写圆括号会导致不同的行为。
return (fn(3)) -- 只返回一个值,而无关于fn()返回几个值
关于多重返回值还要介绍一个特殊函数 unpack
,unpack
接收一个数组作为参数,并从下标1开始返回该数组的所有元素。
-- lua5.2+中全局unpack函数已被移除,并被table.unpack替代。
local unpack = unpack or table.unpack
x,y,z = unpack{1,2,3}
print(x,y,z) -- 1 2 3
unpack
的一项重要用户体现在“泛型调用(generic call)”机制中,泛型调用机制可动态地以任何实参来调用任何函数。
-- lua5.2+中全局unpack函数已被移除,并被table.unpack替代。
local unpack = unpack or table.unpack
fn = string.find
tbl = {'hello lua', 'lua'}
print(fn(unpack(tbl))) -- 7 9
变长参数
Lua函数可接受可变数量的参数,和C语言类似。在函数参数列表中使用...
表示函数有可变的参数。
Lua将函数的参数放在一个叫做arg
的表中,除了参数外,arg
表中还有一个域n
表示参数的个数。
function dump(...)
local str = ""
for i,v in ipairs(arg) do
str = str .. tostring(v).."\t"
end
return str.."\n"
end
例如:只想要string.find
返回的第二个值,典型的方法是使用虚变量_
。
local _, x = string.find(str, pattern)
Lua中的函数还可以接受不同数量的实参
例如:返回所有参数的总和
function sum(...)
local sum=0
for i,v in ipairs{...} do
sum = sum + v
end
return sum
end
print(sum(1,2,3,4)) -- 10
参数中3个点...
表示函数可接收不同数量的实参,当函数被调用时,它的所有参数都会被收集到一起。这部分收集起来的实参称为函数的“变长参数(variable arguments)”。函数要访问它的变长参数时,仍需要3个点。但不同的是,此时这个3个点是作为一个表达式来使用的。
function fn(...)
local x,y,z = ...
print(x,y,z)
end
fn(1,2) -- 1 2 nil
fn(1,2,3) -- 1 2 3
表达式...
的行为类似于一个具有多重返回值的函数,它返回的是当前函数的所有变量参数。
function fwrite(fmt, ...)
return io.write(string.format(fmt, ...))
end
fwrite("%s %s %s", 1, 2, 3) -- 1 2 3
Lua中迭代函数参数时,可使用...
参数收集到一个table
中,但变参中函数非法的nil
时,可使用select
函数将其过滤掉。
function filter(...)
for i=1, select("#", ...) do
local arg = select(i, ...)
if arg ~= nil then
print(arg)
end
end
end
-- test
test(1,nil,2,3)-- 1 2 3
具名实参
Lua的函数参数是和位置相关的,调用时会按顺序依次传递给形式参数。有时候,使用名字制定参数是很有用的(命名参数)。
Lua中的参数传递机制和是具有“位置性”的,也就是说在调用函数时,实参时通过它在参数表中的位置与形参匹配起来。
function rename(tbl)
print(tbl, tbl.oriname, tbl.newname)
end
rename{oriname="ori.lua", newname="new.lua"}
Lua中特殊的函数调用语法,当实参只有一个table
构造式时,函数调用中的圆括号是可有可无的。
function rename(arg)
return os.rename(arg.old, arg.new)
end
第一类值
Lua中的函数是带有词法定界(lexical scoping)的第一类值(first-class values)。
"第一类值"是指在Lua中函数和其他值(数值、字符串...)一样,函数可以被存储在变量中,可以存放在表中,可以作为函数的参数,也可以作为函数的返回值。
“词法界定”是指被嵌套的函数可以访问其他外部函数中的变量,这一特性给Lua提供了强大的编程能力。
Lua中关于函数难以理解的是“函数是可以没有名字的,也就是匿名的。”,但提到函数名时,实际上是说以一个指向函数的变量,和其他类型值得变量是一样的。
Lua中函数作为“第一类值”,表示函数可以存储在变量中,可通过参数传递给其他函数,还可作为其他函数的返回值。由于函数在Lua中是一种“第一类值”,所以不仅可将其存储在全局变量中,还可存储在局部变量甚至table
的字段中。
Lua中函数是一种“第一类值(First-Class Value)”,它们具有特定的词法域(Lexical Scoping)。
“第一类值”是什么意思呢?这表示在Lua中函数与其他传统类型的值具有相同的权利。函数可存储到变量中或table
中,可作为实参传递给其他函数,还可作为其他函数的返回值。
“词法域”是什么意思呢?这是指一个函数可以嵌套在另一个函数中,内部的函数可访问外部函数的变量。这项听起来平凡的特性将给语言带来极大的能力,因为它允许在Lua中应用各种函数式语言(functional-language)中强大的编程技术。
a = {p = print}
a.p('hello') -- hello
print = math.sin
a.p(print(1)) -- 0.8414709848079
sin = a.p
sin(10,20) -- 10 20
Lua对函数式编程(functional programming)提供了良好的支持
在Lua中有一个很容易混淆概念是,函数与所有其他值一样都是匿名的,即它们都没有名称。当讨论一个函数名时,实际上是在讨论一个持有某函数的变量。这与其他变量持有各种值的一个道理,可以以多种方式来操作这些变量。
如果说函数是值的话,那是否可以说函数就是由一些表达式创建的呢?是的,事实上在Lua中最常见的是函数编写方式。
function fn(x)
return 2*x
end
-- 简化书写“语法糖”
fn = function(x) return 2*x end
一个函数定义实际就是一条语句,更准确地说是一条赋值语句,这条语句创建了一种类型为“函数”的值。
可将表达式function(x) <body> end
视为一种函数的构造式,类似table
的构造式{}
一样。将这种函数构造式的结果称为一个“匿名函数”,虽然一般情况下,会将函数赋予全局变量,即给予其一个名称。但在某些特殊情况下,仍会需要用到匿名函数。
Lua即可以调用以自身Lua语言编写的函数,又可以调用以C语言编写的函数。
table库
提供了一个函数table.sort
,它接收一个table
并对其中的元素排序。向这种函数就必须支持各种各样可能的排序规则。sort
函数并没有提供所有排序准则,而是提供了一个可选的参数,所谓“次序函数(order function)”。这个函数接收两个元素,并返回在有序情况下第一个元素是否已排在第二个元素之前。
local tbl = {
{name="ip1", ip="210.26,30,34"},
{name="ip2", ip="210.26,30,33"},
{name="ip3", ip="210.26,30,12"},
}
table.sort(tbl, function(x,y)
return x.name > y.name
end)
for k,v in pairs(tbl) do
print(v.name) --ip3 ip2 ip1
end
像sort
这样的函数,接收另一个函数作为实参,称其为“高阶函数(higher-order function)”。高阶函数是一种强大的编程机制,应用匿名函数来创建高阶函数所需的实参则可以带来更大的灵活性。但请记住,高阶函数并没有什么特权。Lua强调将函数视为“第一类值”,所以高阶函数只是一种基于该观点的应用体现而已。
例如:关于导数的高阶函数
function derivative(fn, delta)
delta = delta or 1e-4
return function(x)
return (fn(x+delta)-fn(x))/delta
end
end
fn = derivative(math.sin)
print(math.cos(10), fn(10)) // -0.83907152907645 -0.83904432662041
闭包函数
当一个函数内部嵌套另一个函数定义时,内部的函数体可以访问外部函数的局部变量,这种特征称之为“词法界定”。词法界定加上第一类函数在编程语言中是一个功能强大的工具。
若将函数写在另一个函数何内,那么这个位于内部的函数便可访问外部函数中的局部变量,这项特征称之为“词法域”。
例如:根据每个学生的年级来对它们姓名进行由高到低的排序
local userlist = {
{username="mary", score=81},
{username="shiva", score=92},
{username="seth", score=65}
}
table.sort(userlist, function(x, y)
return x.score > y.score
end)
for k,v in pairs(userlist) do
print(k, v.username, v.score)
end
--[[
1 shiva 92
2 mary 81
3 seth 65
--]]
创建函数完成操作
function sort_by_grade(names, grades)
table.sort(names,
function(a,b)
return grades[a] > grades[b]
end
);
end
names = {"alice", "peter", "paul", "mary"}
grades = {alice=10, peter=5, paul=9, mary=2}
sort_by_grade(names,grades)
for key,val in pairs(names) do
print(key, val, grades[val])
end
有趣的是,传递给sort
匿名函数可以访问参数grades
,而grades
是外部函数sort_by_grade
的局部变量。在这个匿名函数内部,grades
即不是全局变量也不是局部变量,将其称为一个非局部的变量(non-local variable)或upvalues
或“外部的局部变量(external local variable)”。为什么在Lua中允许这种访问呢?原因在于函数是“第一类值”。
function count()
local i = 0
return function()
i = i + 1
return i
end
end
cnt = count()
print(cnt()) -- 1
print(cnt()) -- 2
print(cnt()) -- 3
匿名函数访问了一个“非局部的变量i
,i
变量用于保持一个计数器。Lua会以closure
的概念来正确地处理这种情况。简单来说,一个closure
就是一个函数加上该函数所需访问的所有“非局部的变量”。如果再次调用count()
,那么它会创建一个新的局部变量i
,从而也将得到一个新的closure
。
技术上来讲,闭包指的是值而不是函数,函数仅仅是闭包的原型声明,尽管如此,在不会导致混淆的情况下,使用术语函数代指闭包。
从技术上讲,Lua中只有closure
,而不存在函数。因此,函数本身就是一种特殊的closure
。不过只要不会引起混淆,仍将采用术语函数来指代closure
。
在许多场合中closure
都是一种很有价值的工具,例如:可作为sort
类高阶函数的参数。closure
对于创建其他函数也很有价值。这种机制使Lua可混合在函数式编程世界中久经考验的编程技术。另外closure
对于回调函数也很有用。
例如,假设有一个传统的GUI工具包可以创建按钮,每个按钮都有一个回调函数,每当用户按下按钮是GUI工具包都会调用这些回调函数。再假设,基于此要做一个十进制计算器,其中需要10个数字按钮,你会发现这些按钮之间的区别其实并不大,仅需在按下不同按钮时稍微不同的操作就可以了。
-- 创建按钮
function digitButton(digit)
return Button{label=tostring(digit), action=function() add_to_display(digit) end}
end
假设Button
是工具包中一个用于创建新按钮的函数,label是按钮的标签,action
是回调closure
,每当按钮按下时就会调用它。回调一般发生在digitButton
函数执行完后,那时局部变量digit
已经超出了作用范围,但closure
仍可以访问到这个变量。
closure
在另一种情况中也非常有用,例如在Lua中函数是存储在普通变量中的,因此可以轻易地重新定义某些函数,甚至是重新定义那些预定义的函数。这也是Lua相当灵活的原因之一。
通常当重新定义一个函数时,需要在新的实现中调用原来的那个函数。举例来说,假设要重新定义函数sin,使其参数能使用角度来替代原先的弧度。那么这个新函数就必须得转换它的实参,并调用原来的sin函数完成真正的计算。
oldSin = math.oldSin
math.sin = function(x)
return oldSin(x*math.pi/180)
end
还有一种更彻底的做法
do
local oldSin = math.sin
local k = math.pi/180
math.sin = function(x)
return oldSin(x*k)
end
end
将老版本的sin
保存到一个私有变量,现在只有通过新版本的sin才能访问到它。
可以使用同样的技术来创建一个安全的运行环境,即所谓的沙盒(sandbox)。当执行一些未受信任的代码时就需要一个安全的运行环境,例如在服务器中执行那些从Internet上接收到的代码。
举例来说,如果要限制一个程序访问文件的话,只需要使用closure来重新定义函数io.open()
即可。
do
local oldOpen = io.open
local accessOK = function(filename,mode)
-- 检查访问权限
end
io.open =function(filename, mode)
if accessOK(filename, mode) then
return oldOpen(filename,mode)
else
return nil,"access denied"
end
end
end
经过重新定义后,一个函数就只能通过受限版本来调用原来那个未受限的open()
函数了。将原来不安全的版本保存到closure的一个私有变量中,从而使得外部再也无法直接访问到原来的版本了。
通过这种技术,可以再Lua的语言层面上就构建出一个安全的运行环境,且不失建议性和灵活性。相对于提供一套大而全的解决方案,Lua提供的则是一套“元机制(meta-mechanism)”,因此可以根据特定的安全需要来创建一个安全的运行环境。
非全局的函数
由于函数是一种“第一类值”,因此一个显而易见的推论是,函数不仅可以存储在全局变量中,也可以存储在table的字段中和局部变量中。
Lua中大部分库采用将函数存储在table
字段中这种机制。若要在Lua中创建这种函数,只需将常规的函数语法和table
语法结合即可。
Lib = {}
Lib.foo = function(x,y)
return x+y
end
Lib.bar = function(x,y)
return x-y
end
当然,也可使用构造式:
Lib = {
foo=function(x,y) return x+y end,
bar=function(x,y) return x-y end
}
只要将一个函数存储到一个局部变量中,即得到一个局部函数(local function),也就是说该函数只能在某个特定的作用域中使用。
对于程序包(package)而言,这种函数定义是非常有用的,因为Lua是将每个特定程序块(chunk)作为一个函数来处理的,所以在一个程序块中声明的函数就是局部函数,这些局部函数只在该程序块中可见。词法域确保了程序包中的其他函数可以使用这些局部函数。
local fn = function(arg)
-- function body
end
local func = function(arg)
fn()
end
对于局部函数的定义,Lua支持一种特殊的语法糖:
local function fn(arg)
-- function body
end
在定义递归的局部函数中,有一个特别之处需要注意。
local face = function(n)
if n==0 then
return 1
else
-- 错误的递归调用
-- 当Lua编译到函数体中调用fact(n-1)的地方时,由于局部的fact尚未定义完毕。
-- 因此这句表达式其实调用了一个全局的fact,而非此函数本身。
return n*face(n-1)
end
end
--[[结局方案:可以先定义一个局部变量,然后再定义函数本身。--]]
local fact
fact = function(n)
if n==0 then return 1
else return n*fact(n-1)
end
end
--[[
现在函数中的fact调用就表示了局部变量,即使在函数定义时,这个局部变量的值尚未完成定义,但之后在函数执行时,fact则肯定拥有了正确的值。
--]]
当Lua展开局部函数定义的“语法糖”时,并不是使用基础函数定义语法。而是对于局部函数定义:
local function fn(<args>) <function body> end
Lua将其展开为
local fn
fn = function(<args>) <function body> end
因此,使用此种语法定义递归函数不会产生错误:
local function fact(n)
if n==0 then return 1
else return n*fact(n-1)
end
end
当然,这个技巧对于间接递归的函数而言是无效的。对于间接递归的情况下,必须使用一个明确的向前声明(Forward Declaration):
local fn,func -- 向前声明
function func()
fn()
end
function fn()
func()
end
--[[
注意,别把第二个函数定义为"local function fn"。
如果那样的话,Lua会创建一个全新的局部变量fn。
而将原来声明的fn(func函数中所引用的那个)置于未定义状态。
--]]
尾调用
Lua函数有一个有趣的特征,那就是Lua支持“尾调用消除(tail-call elimination)”。
所谓“尾调用(tail call)”就是一种类似于goto()
的函数调用,当一个函数调用是另一个函数的最后一个动作时,该调用才算是一条“尾调用”。
function fn(x)
return func(x)
end
--[[
当fn()函数调用完func()函数之后就再无其他事情可做了
因此在这种情况下,程序就不需要返回那个“尾调用”所在的函数了。
所以在“尾调用”之后,程序也不需要保存任何关于该函数的栈(stack)信息。
当func()函数返回时,执行控制权可以直接返回给调用fn()函数的那个点上。
有些语言实现(例如Lua解释器)可以得益于这个特点,使得在进行尾调用时不耗费任何栈空间。
将这种实现称之为支持“尾调用消除”。
--]]
由于“尾调用”不会耗费栈空间,所以一个程序可以拥有无数嵌套的“尾调用”。
function fn(n)
if n>0 then
return fn(n-1)
end
end
--[[
在调用fn()函数时,传入任何数字n作为参数都不会造成栈溢出。
--]]
有一点需要注意的是,当想要受益于“尾调用消除”时,务必要确定当前的调用时一条“尾调用”。判断的准则是“一个函数在调用完另一个函数之后,是否就无其他事情需要做了”。有些看似“尾调用”的代码,其实都违背了这条准则。
function fn(x)
func(x)
end
--[[
当调用完func()函数后,fn()函数并不能立即返回,它还需丢弃func()函数返回的临时结果。
--]]
Lua中,只有return <func>(<args>)
这样的调用形式才算是一条“尾调用”。
return fn(x)+1 -- 必须做一次加法
return x or fn(x) -- 必须调整为一个返回值
return (g(x)) -- 必须调整为一个返回值
Lua在调用前对<func>
及其参数求值,所以它们可以是任意复杂的表达式。
return x[i].fn(x[j]+a*b, i+j)
一条“尾调用”就好比是一条goto
语句。因此,在Lua中“尾调用”的一大应用就是编写“状态机(state machine)”。这种程序通常以一个函数来表示一个状态,改变状态就是goto
(或调用)到另一个特定的函数和。
例如:一个简单的迷宫游戏中,一个迷宫有几间房间,每间房中最多有东南西北4扇门。用户在每一步异动中都需要输入一个移动的方向。如果在某个方向上有门,那么用户可以进入相应的房间。不然,程序就打印一条警告。游戏的目标就是让用户从最初的房间走到最终的房间。
这个游戏就是典型的状态机,其中当前房间就是一个状态。可以将迷宫的每间房实现为一个函数,并使用“尾调用”来实现从一间房移动到另一件。
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=='ease' then
return room4()
else
print("invalid move")
return room3()
end
end
function room4()
print("congratulations")
end
-- 调用初始房间来开始游戏
room1()
--[[
若没有“尾调用消除”,每次用户的异动都会创建一个新的栈层(stack level)
异动若干步后就有可能会导致栈溢出
“尾调用消除”则对用户异动的次数没有任何限制
因为每次异动实际上都只是完成一条goto语句到另一个函数,而非传统的函数调用。
--]]
对于简单的游戏而言,或许觉得将程序设计为数据驱动的会更好一些,其中将房间和异动记录在table中。不过,如果游戏中每间房间都有各自特殊的情况的话,采用这种状态机的设计则更为合适。
Lua迭代器与闭包
迭代器是一种可以遍历(iterate over)集合中所有元素的机制。在Lua中通常将迭代器表示为函数,每次调用函数即返回集合中的“下一个”元素。
每个迭代器都需要在每次成功调用之间保持一些状态,这样才能知道它所在的位置及如何步进到下一个位置。闭包(closure)对于这类任务提供极佳的支持,一个闭包就是一种可以访问其外部嵌套环境中的局部变量的函数。
对于闭包而言,这些变量就可用于在成功调用之间保持状态值,从而使闭包可以记住它在一次遍历中所在的位置。
当然,为了创建一个新的闭包,还必须创建它的“非局部变量(non-local variable)”。因此,一个闭包结构通常涉及到两个函数:闭包本身和一个用于创建该闭包的工厂函数。
-- 为列表编写简单的迭代器,与ipaires不同的是该迭代器并不是返回每个元素的索引,而是返回元素的值。
-- values是一个工厂,每当调用这个工厂时,就创建一个新的闭包(迭代器本身)。这个闭包将它的状态保存在其外部变量tbl和i中。
function values(tbl)
local i=0
return function()
i = i+1
return tbl[i]
end
end
-- 每当调用这个迭代器时,它就从列表tbl中返回下一个值。
-- 直到最后一个元素返回后,迭代器就会返回nil,以此表示迭代器的结束。
tbl={1,2,3,4}
iter = values(tbl) -- 创建迭代器
while true do
local el = iter() -- 调用迭代器
if el==nil then
break
end
print(el)
end
然而,使用泛型for
则更为简单,你会发现泛型for正是为这种迭代而设计的。
function values(tbl)
local i=0
return function()
i = i+1
return tbl[i]
end
end
-- 泛型for为一次迭代循环做了所有的薄记工作。
-- 它在内部保存了迭代器函数,因此不再需要iter变量。
-- 它在每次新迭代时调用迭代器,并在迭代器返回nil时结束循环。
tbl={1,2,3,4}
for el in values(tbl) do
print(el)
end
需求:遍历当前输入文件中所有单词的迭代器
为完成这样的遍历,需要保持两个值:当前行的内容、当前行所处的位置。有了这些信息就可以不断产生下一个单词。迭代器函数的主要部分使用 string.find
在当前行中以当前位置作为起始来所搜索一个单词。使用模式%w+
来描述一个单词,它用于匹配一个或多个的文字/数字字符。如果string.find
找到了一个单词,迭代器就会将当前位置更新为该单词之后的第一个字符,并返回该单词。否认则,它就读取新的一行并反复这个搜索过程。若没有剩余的行,则返回nil
表示迭代的结束。
function allwords()
local line = io.read()
local pos = 1
return function()
while line do
local s,e = string.find(line, "%w+", pos)
if s then
pos = e+1
return string.sub(line,s,e)
else
line = io.read()
pos = 1
end
end
return nil
end
end
for word in allwords() do
print(word)
end