# 引言

Lua 中,函数是对语句和表达式进行抽象的主要方法。既可以用来处理一些特殊的工作,也可以用来计算一些值。 Lua 提供了许多的内建函数,你可以很方便的在程序中调用它们,如 print() 函数可以将传入的参数打印在控制台上。

Lua 函数主要有两种用途:

  1. 完成指定的任务,这种情况下函数作为调用语句使用
  2. 计算并返回值,这种情况下函数作为赋值语句的表达式使用

本篇博客,主要讲解了 lua 中的基本函数,闭包,尾调用,迭代器等知识点

# Lua 函数基本特性

# 函数定义

Lua 编程语言函数定义格式如

optional_function_scope function function_name( argument1, argument2, argument3..., argumentn)
    function_body
    return result_params_comma_separated
end
parame解释
optional_function_scope该参数是可选的制定函数是全局函数还是局部函数,未设置该参数默认为全局函数,如果你需要设置函数为局部函数需要使用关键字 local
function_name指定函数名称。
argument1, argument2, argument3..., argumentn函数参数,多个参数以逗号隔开,函数也可以不带参数。
function_body函数体,函数中需要执行的代码语句块。
result_params_comma_separated函数返回值,Lua 语言函数可以返回多个值,每个值以逗号隔开。当然也可以没有,函数主要用来完成指定的任务

下面看一个简单的例子

function add(a, b)
    print(a + b)
    return a + b
end

此外你可以把函数当做变量传递,或者保存在一个变量之中,就像 C 的函数指针一样

-- out 作为一个外部传入的参数打印一些日志
afunc = function max(a, b, out)
    if a > b then
        return a
    else
        return b
    out()
    end
end
afunc(a, b)

# 多返回值 & 可变参数

多返回值与可变参数可以说是 Lua 的一个与众不同的特性

对与多返回值以参数列表形式列举出来就 ok , 对与可变参数通过 ... 传入, Lua 根据实际情况进行参数的匹配,也遵循多弃少补的原则

代码实例

-- [多返回值] 找出一个数组的最大值,返回最大值和最大值下标
function findMax(num)
    local index = 1
    local maxn = num[1]
    for i = 1, #num do
        if num[i] > maxn then
            index = i
            maxn = num[i]
        end
    end
    return maxn, index
end
-- [可变参数] 计算一组数值的和
function sum(...)
    local s = 0
    for i, v in ipairs{ ... } do
        s = s + v
    end
    print(s)
    -- 使用 select 得到第 i 个参数的值
    for i = 1, select('#', ...) do
        local args = select(i, ...)
        print(args)
    end
end
print(findMax({1, 3, 5, 2, 4}))
sum(1, 2, 3, 4, 5, 6)
-- output ------
5   3
21
1
2
3
4
5
6

变长参数 ( ... ) 搭配 unpack ( lua5.3 已经把这个函数移动到了 table 表中,即使用 table.unpack 调用) 这个库函数使用能适用于传入多个形参同时传出多个形参做一些其他操作的特殊情况

local out = function(a, b, c)
    print(a)
    print(b)
    print(c)
end
function tmp(a, ...)
    local t1 = {}
    for k, v in ipairs{ ... } do
        --print(v)
        table.insert(t1, v)
    end
    if nil ~= next(t1) then
        a(table.unpack(t1))
    end
end
tmp(out, 1, 2, "lua")
-- output
1
2
lua

# 具名实参

一般的语言中,参数传递是具有位置性的,有就是说在参数传递后通过在参数列表的位置中与形参匹配起来,第一个值以第一个形参匹配,第二个值与第二个形参匹配以此类推,但有时候难免记不起函数的参数相对位置,那么通过名字来匹配是一个有效的策略。

lua 通过构造一个 table 表,将参数封装在 table 中当做参数传递,就可以通过名字到 table 表中读取

一个简单的例子

-- 通过系统 os.rename 来更改文件名字
args = {old = "tmp.lua", new  = "now.lua"}
function rename(arg)
    return os.rename(arg.old, arg.new)
end

# 闭包函数

在了解闭包之前我们先来了解一下什么是匿名函数

# 匿名函数

所谓匿名,字面意思理解就是没有指定名称的函数,而事实上也的确如此

传统的一个函数定义如下 function foo (x) return 2 * x end 但是这其实就是 foo = function(x) return 2 * x end 的一种简写,即一种所谓的 “语法糖”, 因此一个函数定义实质就是一条赋值语句,这条语句创建了一种类型为 “函数” 的值,并赋值给一个变量。可以将表达式 function (x) <body> end 视为一种函数构造式,就像 table 的构造式 {} 一样。这里的 function(x) return 2 * x end 就可以称之为一个匿名函数

# 闭合函数

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

table.sort(names,functin (n1,n2)
    return grades[n1]>grades[n2]
end)
-- 内部匿名函数可以访问外部函数的 n1,n2

而闭包 ( closure ) 是由一个函数和该函数会访问到的非局部变量 (或者是 upvalue ) 组成的,其中非局部变量 ( non-local variable ) 是指不是在局部作用范围内定义的一个变量,但同时又不是一个全局变量,主要应用在嵌套函数和匿名函数里,因此若一个闭包没有会访问的非局部变量,那么它就是通常说的函数。也就是说,在 Lua 中,函数是闭包一种特殊情况。

简单总结如下

闭包:通过调用含有一个内部函数加上该外部函数持有的外部局部变量 ( upvalue ) 的外部函数 (就是工厂) 产生的一个实例函数

闭包组成: 外部函数 + 外部函数 创建的 upvalue + 内部函数(闭包函数)

举一个例子说明一下

function newCount()
    local i = 0
    return function()
        i = i + 1
        return i
    end
end
c1 = newCount()
print(c1())
print(c1())
c2 = newCount()
print(c2())
print(c2())
print(c1())
-- output
1
2
1
2
3

在上面这个计数器的代码中,匿名函数访问了一个 "非局部变量" i , 该变量用于保持一个计数器,初看上去,由于创建变量 i 的函数 ( newCounter ) 已经返回,所以之后每次调用匿名函数时, i 都应是已超出了作用范围的。但其实不然, Lua 会以 closure 的概念来正确地处理这种情况,这儿的 i 也就是一个 upvalue 。如果再次调用 newCounter , 那么它会创建一个新的局部变量 i , 从而也将得到一个新的 closure

利用 closureupvalue 的特性, lua 还可以实现一种类似于沙盒功能的程序。沙盒为外部程序的运行提供了一个安全受限的环境,比如常见的在 Android 系统中,安装的应用程序部分是在沙盒中运行的,这些应用程序调用了 Android 系统的 api (如打开和读写文件), 在沙盒中运行这些应用程序时,应用程序调用的系统 api 往往被重写过,加上了限制条件,如果应用程序通过了条件的限制,就能成功调用,否则将调用失败。

lua 中,假设有外部程序在 lua 的环境中运行,会调用打开文件的 api , 使用 closure 来重定义这些 api 就能实现沙盒的功能。

do
    local oldOPen = io.open
    local access_OK = function(filename, mode)
        <检查访问权限>
    end
    io.open = function(filename, mode)
        if access_OK(filename, mode) then
            return oldOpen(filename, mode)
        else
            return nil, "access denied"
        end
    end
end

# 非全局函数

通常我们在 lua 中声明和定义一个函数时,不会加上 local 变量,此时函数是全局函数。在内存中可以被其他文件调用。 举个例子,假设现在有两个 lua 文件 1.lua , 2.lua , 在 1.lua 中有两个全局函数 a , b 以及一个局部函数 c , 在 2.lua 中对 1.lua 的函数进行调用,函数 a , b 将能成功访问,而 c 函数将由于访问权限问题无法访问

具体代码如下

-- 1.lua
function a()
    print("I\'m func a!")
end
function b()
    print("I\'m func b!")
end
function c()
    print("I\'m local func c!")
end
-- 2.lua
package.path = package.path .. ";...?.lua" -- 目录文件 (瞎写的意思意思 ==)
require "1"
a()         -->> ok
b()         -->> ok
c()         -->> error

在比如说,递归是我们经常用到的一个编程思想,假如我有如下函数

local fact = function(n)
    if n == 0 then
        return 1
    else
        return n * fact(n - 1)  -->> error
    end
end

看上去好像并没有什么错误,但是事实上调用发现出错了,这是由于 Lua 在编译 fact 时编译到内层 fact(n-1) 发现找不到 fact 的定义造成的,这一句语句 lua 实际上调用了一个全局的 fact , 而非这个函数自身,要解决这个问题,把函数写成全局就 ok (只是可行,不建议), 或者像 c 或者 c++ 一下事先申明一下就好了,即

local fact
fact = function(n)
    if n == 0 then
        return 1
    else
        return n * fact(n - 1)
    end
end
print(fact(5))
-- output
120

# 尾调用

# 尾调用定义

尾调用也可以称之为 "尾递归", 即递归的最后一个动作是对一个函数的引用,由于当前的递归函数最后一个动作是对一个函数的引用,因此当前的递归函数的上下文对于递归结果已经不重要,在进入对下一个函数的引用时,会把保存在堆栈中的当前递归函数的上下文环境清除,把空间让给下一层递归或函数。

# lua 中的尾调用

lua 语言同样支持尾调用,实现尾调用时,如果希望保存当前局部变量值,需在当前递归函数的最后一个动作将当前需要保存的变量或环境打包为参数传递给下一层递归或函数,并且由于尾调用不会耗费栈空间,所以一个程序可以拥有无数嵌套的 “尾调用”

举个例子

-- 一个基本的尾调用
function() f(x) return (x) end
-- 这个函数传入任何的 n 都不会造成栈溢出
function foo(n)
    if n > 0 then
        return foo(n - 1)
    end
end

当然对于 “尾调用” 很多人都存在一个思维误区,下面的几种情况 (假设都是函数 f 的返回) 都不是尾调用

return g(x) + 1 -- 必须做一次加法

return x or g(x) -- 必须调整为一个返回值

return (g(x)) -- 必须调整为一个返回值

原因在于,当调用完 g 函数后 f 不能够立即返回,他还要丢弃 f 返回的临时结果,在 Lua 中只有对 return <func>(<args>) 这样的调用形式才能算一个尾调用

尾调用的意义在于哪怕他是无限调用的代码,程序运行时内存消耗都能保持一个相对稳定的状态,不存在崩溃的可能 (当然代码写的没问题 orz)

可以用下列代码的两个函数分别跑着看看,体会尾调用的好处

-- 无尾调用消除的无限递归
function recursion0(n)
    if 1 == n then
        print("----------------")
    end
    print("Memory:" .. collectgarbage("count"))
    return n + recursion0(n - 1)
end
recursion0(5)
-- output
-- 可以看到内存值一直增大直到崩溃
-- 使用尾调用消除的无限递归
function recursion1(n)
    if 1 == n then
        print("----------------")
    end
    print("Memory:" .. collectgarbage("count"))
    return recursion1(n - 1)
end
recursion1(5)
-- output
-- 内存消耗维持在一个相对稳定的水平上

Hint: 尾调用一般应用在实现游戏状态机或实现广度、深度搜索等方面是非常有用的,得益于尾调用的内存回收,使用了这种类似实现的程序能有更大的空间去完成多种游戏状态的穷举。

# 迭代器与泛型 for

所谓迭代器就是一种能够遍历一种集合中所有元素的机制,每个迭代器对象代表容器中的确定的地址 在 Lua 中迭代器是一种支持指针类型的结构,通常将迭代器表示为函数,每调用一个函数,即返回集合中的 “下一个” 元素

# 迭代器与 closure

迭代器一般都涉及到相关状态的保存,所以迭代器和 closure 的关系就不言而喻了,下面给出一个简单的迭代器例子

function values(t)
    local i = 0
    return function() i = i + 1; return t[i] end
end
-- 在 while 中使用
t = {10, 20, 30}
iter = values(t)
while true do
    local element = iter()
    if nil == element then break end
    print(element)
end

当然使用 泛型for 来调用这个迭代器更简单,事实上就是为它而设计的

t = {10, 20, 30}
for element in values(t) do
    print(element)
end

泛型for 为一次迭代器循环做了所有的薄记动作,在内部保存了迭代器函数,因此就省去了 iter 这个变量,一般来说迭代器,都具有迭代器本身编写复杂,使用简单的特征

# 泛型 for 迭代器

泛型for 在自己内部保存迭代函数,实际上它保存三个值: 迭代函数状态常量控制变量

泛型for 迭代器提供了集合的 key/value 对,语法格式如下:

for <var-list> in <exp-list> do
    <body>
end
-- <var-list > 是一个或者多个变量的列表,以逗号分隔
-- <exp-list > 是一个或者多个表达式的列表,同样以逗号分隔

一个简单例子如下

array = {"Lua", "Tutorial"}
for key,value in ipairs(array) do
   print(key, value)
end
-- output
Lua
Tutotial

以上实例中我们使用了 Lua 默认提供的迭代函数 ipairs

下面我们看看 泛型for 的执行过程:

  • 首先,初始化,计算 in 后面表达式的值,表达式应该返回 泛型for 需要的三个值:迭代函数、状态常量 ( array )、控制变量 (遍历的下标);与多值赋值一样,如果表达式返回的结果个数不足三个会自动用 nil 补足,多出部分会被忽略
  • 第二,将状态常量和控制变量作为参数调用迭代函数 (注意:对于 for 结构来说,状态常量没有用处,仅仅在初始化时获取他的值并传递给迭代函数)
  • 第三,将迭代函数返回的值赋给变量列表
  • 第四,如果返回的第一个值为 nil 循环结束,否则执行循环体
  • 第五,回到第二步再次调用迭代函数

Lua 中我们常常使用函数来描述迭代器,每次调用该函数就返回集合的下一个元素。 Lua 的迭代器包含以下两种类型:

  • 无状态的迭代器
  • 多状态的迭代器 (复杂状态)

# 无状态的迭代器

“无状态的迭代器” 正如名字说的一样是指不保留任何状态的迭代器,因此在循环中我们可以利用无状态迭代器避免创建闭包花费额外的代价。每一次迭代,迭代函数都是用两个变量 (状态常量和控制变量) 的值作为参数被调用,一个无状态的迭代器只利用这两个值可以获取下一个元素。这种无状态迭代器的典型的简单的例子是 ipairs , 调用 next(t, nil) , 他遍历数组的每一个元素。同时还有一个 pairs 调用 next(t, k) , ktable 中的键值,返回 table 中的下一个 key 及 当前 key 对应的值,这个调用是返回 table 中任意次序的一组值

以下实例我们使用了一个简单的函数来实现迭代器,实现 数字 n 的平方:

function squre(cnt, cur)
    if cur < cnt then
        cur = cur + 1
    return cur, cur*cur
    end
end
for k, v in squre, 3, 0 do
    print(k, v)
end

迭代的状态包括被遍历的表 (循环过程中不会改变的状态常量) 和当前的索引下标 (控制变量), ipairs 和迭代函数都很简单,我们在 Lua 中可以这样实现:

function iter (a, i)
    i = i + 1
    local v = a[i]
    if v then
       return i, v
    end
end
function ipairs (a)
    return iter, a, 0
end

Lua 调用 ipairs(a) 开始循环时,他获取三个值:迭代函数 iter 、状态常量 a 、控制变量初始值 0 ; 然后 Lua 调用 iter(a, 0) 返回 1, a[1](除非a[1]=nil) ; 第二次迭代调用 iter(a, 1) 返回 2,a[2] ...... 直到第一个 nil 元素。

# 多状态的迭代器 (复杂状态)

很多情况下,迭代器需要保存多个状态信息而不是简单的状态常量和控制变量,最简单的方法是使用闭包,还有一种方法就是将所有的状态信息封装到 table 内,将 table 作为迭代器的状态常量,因为这种情况下可以将所有的信息存放在 table 内,所以迭代函数通常不需要第二个参数。

下面我们通过闭包来实现一个迭代器,遍历一个 table

word = {"apple", "banana", "pear"}
function iter(list)
    local cur = 0
    local cnt = #list
    return function()
        cur = cur + 1
        if cur <= cnt then
            return list[cur]
        end
    end
end
for element in iter(word) do
    print(element)
end
-- output
apple
banana
pear

# 结语

以上就是 Lua 中函数相关的一些东西,示例代码直接在用 markdown 写博客的时候手撸,未经严格测试,如有错误欢迎指正

更新于 阅读次数