跳到主要内容
信息
· 文章中可能会出现一些错误,希望大佬们可以在评论区指出;

05-函数

函数(Function)是对语句和表达式进行抽象的主要方式。在Lua语言中,函数是严格遵循词法定界(Lexical Scoping)的第一类值(first-class value)。其中:

  • 第一类值:Lua语言中的函数与其他常见类型(例如数值和字符串)的值具有同等权限。也就是说一个程序可以将某个函数保存到变量(全局/局部均可)或表中,也可以将某个函数作为参数传递给其他函数(高阶函数 higher-order functions),还可以将某个函数作为其他函数的返回值返回。
  • 词法定界:Lua语言中的函数可以访问包含它本身的外部函数中的局部变量。也就是说Lua语言支持Lambda表达式(匿名函数)和闭包(Closure)。

定义函数​

定义函数的语法如下:

function 函数名(参数列表)
函数体
return xxx
end

-- 等价于:
函数名 = function(参数列表)
函数体
return xxx
end

调用函数​

调用函数时使用的参数个数可以与定义函数时使用的参数个数不一致,Lua会抛弃多余参数并将不足的参数设为nil:

function f(a, b)
print(a, b)
end
f() --> nil nil
f(1) --> 1 nil
f(1, 2) --> 1 2
f(1, 2, 3) --> 1 2 (3被丢弃)

这样的特性可以实现函数的默认实参:

function incCount(n)
n = n or 1 -- 如果n为nil, 那么n为1
globalCounter = globalCounter + n
end

此外,当函数只有一个参数且该参数是字符串常量或表构造器时,调用函数时可以省略括号:

print "Hello World" --> Hello World
dofile 'test.lua' --> 执行test.lua文件
f {x = 10, y = 20} --> 调用f函数, 参数为一个表

多返回值​

Lua语言允许一个函数返回多个结果,例如string.find()函数会返回匹配字符串在原字符串中的起始和终止位置:

s, e = string.find("hello Lua", "Lua") --> 7 9

要让自定义函数返回多个值,只需在return后列出所有要返回的值即可:

--- 找到最大元素及其位置
function getMax(a)
local idx = 1
local val = a[idx]
for i = 1, #a do
if a[i] > val then
idx = i
val = a[i]
end
end
return idx, val
end

Lua会根据函数的被调用情况调整返回值的数量:

  • 函数作为一条单独语句调用时,所有返回值都会被丢弃;
  • 函数被作为表达式(例如,加法的操作数)调用时,将只保留第一个返回值;
  • 函数调用是一系列表达式中的 最后/唯一一个 表达式时,其所有的返回值都会被保留;

其中,"一系列表达式"指的是:多重赋值,函数调用时传入的实参列表,表构造器和return语句。详情请参考下列代码:

示例:一系列表达式点击展开/折叠代码
function foo0() end -- 不返回结果
function foo1() return "a" end -- 返回1个结果
function foo2() return "a", "b" end -- 返回2个结果

-- 多重赋值:
-- 函数调用是一系列表达式中的最后/唯一一个表达式, 所有的返回值都会被保留:
x, y = foo2() --> x = "a", y = "b"
x, y = foo1() --> x = "a", y = nil
x, y, z = 10, foo2() --> x = 10, y = "a", z = "b"
-- 函数没有返回值,或者返回值个数不够多,会用nil补充:
x, y = foo0() --> x = nil, y = nil
x, y = foo1() --> x = "a", y = nil
x, y, z = foo2() --> x = "a", y = "b", z = nil
-- 函数调用不是一系列表达式中的最后/唯一一个表达式, 只保留第一个返回值:
x, y = foo2(), 10 --> x = "a", y = 10
x, y = foo0(), 20, 30 --> x = nil, y = 20


-- 函数调用时传入的实参列表:
-- 当一个函数调用是另一个函数调用的最后/唯一一个实参时,所有的返回值都会被传递给第二个函数:
print(foo0()) --> nil
print(foo1()) --> a
print(foo2()) --> a b
-- 函数调用不是另一个函数调用的最后/唯一一个实参时,只保留第一个返回值:
print(foo2(), 10) --> a 10
print(foo2() .. "x") --> ax
-- 括号把多值表达式强制转换为单值表达式,只保留第一个返回值:
print((foo2())) --> a

-- 表构造器:
-- 函数调用是表达式列表中的最后/唯一一个表达式时,所有的返回值都会被保留:
t = {foo0()} --> t = {}
t = {foo1()} --> t = {"a"}
t = {foo2()} --> t = {"a", "b"}
-- 函数调用不是表达式列表中的最后/唯一一个表达式时,只保留第一个返回值:
t = {foo0(), foo2(), 4} --> t = {nil, "a", 4}

-- return语句:
function foo(i)
if i == 0 then
return foo0() -- 返回nil
elseif i == 1 then
return foo1() -- 返回"a"
else
return foo2() -- 返回"a", "b"
end
end
-- 函数调用是表达式列表中的最后/唯一一个表达式时,所有的返回值都会被保留:
print(foo(0)) --> nil
print(foo(1)) --> a
print(foo(2)) --> a b
-- 函数调用不是表达式列表中的最后/唯一一个表达式时,只保留第一个返回值:
print((foo2())) --> a

最后再总结一下:函数调用处于多值上下文(表达式列表的最后一个位置、return 的最后一个表达式、函数调用的最后一个实参等)时保留全部返回值;处于单值上下文(如 .. 操作数、括号内、非最后位置)时只保留第一个返回值。

table.unpack()​

有关函数的多返回值还涉及到一个特殊的函数table.unpack。该函数的参数是一个数组,返回值为数组内的所有元素:

table.unpack({1,2,3}) -- 1 2 3
a, b = table.unpack({1, 2, 3}) -- 1 2 (3被丢弃)

该函数的重要用途之一体现在泛型调用机制中,该机制允许我们调用具有任意参数的任意函数。例如下面两个写法是等价的,但第二种泛型调用写法更通用:

-- 写法1:
string.find("hello", "ll")

-- 写法2:
f = string.find
a = {"hello", "ll"}
f(table.unpack(a))

该函数还有两个参数用来显式限制返回元素的范围:

table.unpack({1, 2, 3}, 2, 3) -- 2 3

此外,还有函数table.pack(...),该函数获取所有可变长参数,将其打包入表中,然后添加一个额外字段"n",内容是表的长度。

可变长函数参数​

遍历参数​

使用...表示该函数的参数是可变长的,例如一个求和函数:

function add(...)
local sum = 0
for _, v in ipairs{...} do
sum = sum + v
end
return sum
end

print(add(1, 2, 3)) -- 6

要想使用参数列表,需要用{...}获取由所有可变长参数组成的列表,然后才能遍历。如果参数列表中包含nil,就不能这样做。需要使用table.pack(...)。

另一种遍历参数的方法是使用函数select,它有一个固定参数selector以及可变参数,有两种用法:

-- selector为数值n, 返回第n个参数和它后面的参数
print(select(2, "a", "b", "c")) -- b c
-- selector为"#", 返回额外参数的总数
print(select("#", "a", "b", "c")) -- 3

应用: 格式化输出文本​

如果想要格式化输出文本,需要使用string.format和io.write函数,我们可以用可变长函数参数将它俩结合起来:

function fwrite(fmt, ...)
return io.write(string.format(fmt, ...))
end

闭包​

在开头提到词法定界(lexical scoping)这一特性,意思是编写一个被其他函数B包含的函数A时,被包含的函数A可以访问包含它的函数B中的局部变量。接下来以一个简单的例子引出闭包(closure)的概念:

function newCounter()
local count = 0
return function() -- 匿名函数
count = count + 1
return count
end
end

c1 = newCounter() -- c1是一个闭包
print(c1()) --> 1
print(c1()) --> 2

在上述代码中,匿名函数访问了一个非局部变量(对它自己来说)count并将其当作计数器。然而,由于创建该变量的函数newCounter()已经返回,因此在调用匿名函数时,该变量似乎已经超出了作用域。但由于闭包(Closure)的存在,该匿名函数仍能正确访问非局部变量count,并且每次调用该匿名函数时,count的值都会被保留。

如果我们再次调用newCounter(),就会创建一个新的闭包,它有自己的count变量:

c2 = newCounter() -- c2是另一个闭包
print(c2()) --> 1
print(c1()) --> 3
print(c2()) --> 2

简单地说,闭包就是 一个函数和它所引用的非局部变量的组合 。闭包在许多场合中都非常有用:

  • 作为 高阶函数的参数 :例如函数table.sort(),该函数的第二个参数为自定义排序函数,可以在这里定义匿名函数使用;

  • 作为 回调函数 :下例通过digitButton()函数创建按钮(包含标签label和回调函数action两个属性),在该函数执行后回调函数才可能被执行,此时就得通过闭包访问变量digit;

    示例:模拟显示器点击展开/折叠代码
    -- 1. 模拟一个“显示屏”
    local display = "" -- 用闭包保存当前显示内容

    local function addToDisplay(text)
    display = display .. text
    end

    local function getDisplay()
    return display
    end

    -- 2. 模拟一个“按钮”
    -- Button 接收一个 table,里面有 label 和 action
    local function Button(spec)
    return {
    label = spec.label,
    action = spec.action,
    }
    end

    -- 3. 创造按钮的函数
    local function digitButton(digit)
    return Button{
    label = tostring(digit),
    action = function()
    addToDisplay(digit)
    end
    }
    end

    -- 4. 创建几个按钮并“点击”它们
    local b1 = digitButton(1)
    local b2 = digitButton(2)
    local b3 = digitButton(3)

    b1.action() -- 相当于点击按钮 1
    b2.action() -- 点击 2
    b3.action() -- 点击 3

    print(getDisplay()) --> 123
  • 重新定义/预定义函数 :下例重新定义了函数sin,将其参数更改为以角度制为单位。

    示例:重新定义math.sin()点击展开/折叠代码
    print(math.sin(math.pi / 6)) --> 0.5

    -- do-end 代码段限制局部变量OldSin的作用域,只有新版本的sin函数才能访问原来的sin函数,
    -- 其他部分的代码则访问不了。
    do
    local oldSin = math.sin
    local k = math.pi / 180
    math.sin = function (x)
    return oldSin(x * k)
    end
    end

    print(math.sin(30)) --> 0.5

    上述示例的进阶应用体现在代码安全方面:在下载未受信任的代码后,可以通过闭包重新定义敏感库函数(例如文件访问函数io.open())让代码使用受限制的库函数。

参考资料​