Lua函数

函数有两种用途:

  1. 完成指定任务,此时函数作为调用语句使用。
  2. 计算并返回值,此时函数作为赋值语句的表达式使用。
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()返回几个值

关于多重返回值还要介绍一个特殊函数 unpackunpack接收一个数组作为参数,并从下标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

匿名函数访问了一个“非局部的变量ii变量用于保持一个计数器。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

推荐阅读更多精彩内容

  • 虽然计算机可以做很多事情,但它不会思考,它需要接受系统化的指令来工作。大部分用户通过应用程序为计算机指派任务,软件...
    JunChow520阅读 4,136评论 0 4
  • 《超级个体-伽蓝214》85/100,1.31日打卡,天气多云 【三件事】 1. 听每日得到音频 2. 阅读+笔记...
    伽蓝214阅读 50评论 0 0
  • 2018年04月26日 星期四 亲子日记第110天 今天孩子进行了期中考试,于是下午我早早回到家,给孩子做...
    梦_0ba6阅读 77评论 0 1
  • 23岁之前从未到过我现在所在的城市,毕业之前“奉命”来到佛山,格外陌生…… 天下着大雨,我从学校出...
    D034雨爱雨_佛山阅读 46评论 1 6