Lua 编程/函数
一个栈 是一个项目的列表,其中项目可以被添加(压入)或删除(弹出),并遵循后进先出原则,这意味着最后一个添加的项目将是第一个被删除的项目。这就是为什么这种列表被称为栈:在一个栈中,你不能移除一个项目,除非先移除位于其顶部的项目。因此,所有操作都在栈的顶部进行。如果一个项目在另一个项目之后添加,则它位于另一个项目的上面;如果它在另一个项目之前添加,则它位于另一个项目的下面。
一个函数(也称为子程序、过程、例程或子程序)是一系列执行特定任务的指令,并且可以在程序的其他地方调用,以便在需要执行该指令序列时执行。函数还可以接收值作为输入,并在可能操作输入或根据输入执行任务后返回输出。函数可以在程序的任何地方定义,包括在其他函数内部,并且也可以从程序的任何可以访问它们的部分调用:函数,就像数字和字符串一样,是值,因此可以存储在变量中,并且具有变量共有的所有属性。这些特性使函数非常有用。
因为函数可以从其他函数中调用,所以 Lua 解释器(读取和执行 Lua 代码的程序)需要能够知道当前正在执行的函数是由哪个函数调用的,以便在函数终止(没有更多代码要执行)时,它可以返回到正确函数的执行。这是通过一个称为调用栈的栈来完成的:调用栈中的每个项目都是一个调用其正上方栈中函数的函数,直到栈中的最后一个项目,即当前正在执行的函数。当一个函数终止时,解释器使用栈的弹出操作来移除列表中的最后一个函数,然后返回到前一个函数。
函数有两种类型:内置函数和用户定义函数。内置函数 是 Lua 提供的函数,包括诸如您已经知道的print
函数等函数。一些函数可以直接访问,比如print
函数,但其他的需要通过库来访问,比如math.random
函数,它返回一个随机数。用户定义函数 是用户定义的函数。用户定义函数使用函数构造函数定义
local func = function(first_parameter, second_parameter, third_parameter)
-- function body (a function's body is the code it contains)
end
上面的代码创建了一个具有三个参数的函数,并将其存储在变量func中。下面的代码与上面的代码完全相同,但使用了定义函数的语法糖
local function func(first_parameter, second_parameter, third_parameter)
-- function body
end
需要注意的是,当使用第二种形式时,可以从函数内部引用该函数,而使用第一种形式则无法做到这一点。这是因为local function foo() end
转换为local foo; foo = function() end
而不是local foo = function() end
。这意味着在第二种形式中foo是函数环境的一部分,而在第一种形式中则不是,这解释了为什么第二种形式使得能够引用函数本身。
在这两种情况下,都可以省略local
关键字将函数存储在全局变量中。参数就像变量一样,允许函数接收值。当一个函数被调用时,可以向其传递参数。然后,函数将接收它们作为参数。参数就像在函数开头定义的局部变量一样,并将根据传递给函数调用的参数的顺序依次赋值;如果缺少参数,则参数的值将为nil
。以下示例中的函数将两个数字相加并打印结果。因此,当代码运行时,它将打印5。
local function add(first_number, second_number)
print(first_number + second_number)
end
add(2, 3)
函数调用通常采用name(arguments)
的形式。但是,如果只有一个参数,并且它是一个表或字符串,并且它不在变量中(意味着它是在函数调用中直接构造的,表示为字面量),则可以省略括号
print "Hello, world!"
print {4, 5}
前一个示例中的第二行代码将打印表的内存地址。当将值转换为字符串时(print
函数会自动执行此操作),复杂类型(函数、表、用户数据和线程)将更改为其内存地址。但是,布尔值、数字和 nil 值将转换为相应的字符串。
在实践中,术语参数和实参经常互换使用。在这本书中,以及在它们正确的含义中,参数和实参分别表示对应实参的值将被赋值到的名称,以及传递给函数以赋值给参数的值。
函数可以接收输入,对其进行操作并返回输出。您已经知道它们如何接收输入(参数)和操作它(函数体)。它们还可以通过返回任何类型的零个或多个值来提供输出,这是使用return语句完成的。这就是为什么函数调用既是语句又是表达式:它们可以被执行,但也可以被求值。
local function add(first_number, second_number)
return first_number + second_number
end
print(add(5, 6))
上面函数中的代码将首先定义函数add
。然后,它将使用5和6作为值来调用它。该函数将对它们进行加法运算并返回结果,然后打印该结果。这就是为什么上面的代码会打印11。一个函数也可以通过用逗号分隔计算这些值的表达式来返回多个值。
用户可以定义自己的函数,并根据需要对其进行自定义。
除了获取参数和返回值之外,用户定义的函数还可以具有副作用,即由函数执行引起的变量或程序状态的变化。[1]
错误有三种类型:语法错误、静态语义错误和语义错误。语法错误发生在代码明显无效时。例如,下面的代码将被 Lua 检测为无效
print(5 ++ 4 return)
上面的代码没有意义;不可能从中获得任何含义。类似地,在英语中,“cat dog tree”在语法上无效,因为它没有意义。它不遵循创建句子的规则。
静态语义错误发生在代码有意义但仍然没有意义时。例如,如果您尝试将字符串与数字相加,则会得到一个静态语义错误,因为不可能将字符串与数字相加
print("hello" + 5)
上面的代码遵循 Lua 的语法规则,但它仍然没有意义,因为它不可能将字符串与数字相加(除非字符串表示数字,在这种情况下,它将被强制转换为数字)。这可以比作英语中的句子“I are big”。它遵循英语创建句子的规则,但它仍然没有意义,因为“I”是单数,“are”是复数。
最后,语义错误是在代码片段的含义与其创建者认为的含义不符时发生的错误。这些是最糟糕的错误,因为它们可能非常难以发现。Lua 总是会在出现语法错误或静态语义错误时告诉您(这称为抛出错误),但它无法告诉您何时出现语义错误,因为它不知道您认为代码的含义是什么。这些错误发生的频率比大多数人认为的要高,并且查找和纠正它们是许多程序员花费大量时间做的事情。
查找错误并对其进行纠正的过程称为调试。大多数情况下,程序员会花费更多时间查找错误而不是实际纠正错误。这对所有类型的错误都适用。一旦您知道问题是什么,修复它通常很简单,但有时,程序员可能会查看一段代码几个小时而找不到其中的错误。
抛出错误是指指示代码存在问题(无论是由解释器手动执行还是由解释器(读取和执行代码的程序)自动执行)。当给定的代码无效时,Lua 会自动执行此操作,但也可以使用error
函数手动执行
local variable = 500
if variable % 5 ~= 0 then
error("It must be possible to divide the value of the variable by 5 without obtaining a decimal number.")
end
error
函数还有一个第二个参数,用于指示应该在哪个栈层级抛出错误,但本书不会介绍它。assert
函数的功能与 error
函数相同,但只有当它的第一个参数计算结果为 nil 或 false 时才会抛出错误,并且它没有参数可以用来指定应该在哪个栈层级抛出错误。例如,assert
函数在脚本开头很有用,可以检查脚本运行所需的库是否可用。
可能很难理解为什么有人会想要主动抛出错误,因为程序中的代码在抛出错误时会停止运行。但是,当函数使用不正确或程序未在正确的环境中运行时抛出错误,通常可以帮助调试代码的人员立即找到错误,而无需长时间盯着代码却不知道问题出在哪里。
有时,防止错误停止代码并改做其他事情(例如向用户显示错误消息以便他可以向开发人员报告错误)可能很有用。这称为异常处理(或错误处理),通过捕获错误以防止其传播并运行异常处理程序来处理错误。在不同的编程语言中,实现方法差异很大。在 Lua 中,它是使用受保护调用来实现的。[2] 它们被称为受保护调用,因为在受保护模式下调用的函数在发生错误时不会停止程序。有两个函数可以用来在受保护模式下调用函数
函数 | 描述 |
---|---|
pcall(function, ...) |
在受保护模式下调用函数,并返回一个状态代码(一个布尔值,其值取决于是否抛出错误)以及函数返回的值,或者如果函数因错误而停止,则返回错误消息。可以通过在第一个参数(应该在受保护模式下调用的函数)之后将参数传递给 pcall 函数来向函数传递参数。 |
xpcall(function, handler, ...) |
与 pcall 做同样的事情,但是当函数出错时,它不会返回与 pcall 返回相同的值,而是使用这些值作为参数调用 handler 函数。然后可以使用 handler 函数(例如)来显示错误消息。与 pcall 函数一样,可以通过传递给 xpcall 函数来向函数传递参数。 |
之前提到过调用栈,它是一个包含所有已调用函数的栈,按照调用顺序排列。大多数语言(包括 Lua)中的调用栈都有最大大小。这个最大大小非常大,在大多数情况下都不需要担心,但是如果没有任何东西可以阻止函数无限次地调用自身(这称为递归,这样的函数称为递归函数),那么这些函数可能会达到此限制。这称为栈溢出。当栈溢出时,代码会停止运行并抛出错误。
可变参数函数(也称为 vararg 函数)是可以接受可变数量参数的函数。可变参数函数在其参数列表的末尾用三个点 ("...") 表示。不适合参数列表中参数的参数不会被丢弃,而是通过 vararg 表达式提供给函数,该表达式也用三个点表示。vararg 表达式的值为一个值列表(而不是表),然后可以使用以下表达式将其放入表中以便更容易地操作:{...}
。在 Lua 5.0 中,额外的参数不是通过 vararg 表达式提供,而是通过一个名为“arg”的特殊参数提供。以下函数是一个示例,它会将第一个参数添加到它接收的所有参数中,然后将它们全部加起来并打印结果
function add_one(increment, ...)
local result = 0
for _, number in next, {...} do
result = result + number + increment
end
end
无需理解上面的代码,它只是一个可变参数函数的演示。
select
函数可用于操作参数列表而无需使用表。它本身就是一个可变参数函数,因为它接受无限数量的参数。它返回第一个参数指定的数字后的所有参数(如果给定的数字为负数,则从末尾开始索引,这意味着 -1 是最后一个参数)。如果第一个参数是字符串“#”,它还会返回它接收到的参数数量(不包括第一个)。它可以用来丢弃参数列表中某个数字之前的全部参数,并且更重要的是,可以区分作为参数发送的 nil 值和未发送任何值的情况。实际上,当“#”作为第一个参数给出时,select
会区分 nil 值和无值。参数列表(以及返回值列表)是元组的实例,将在关于表的章节中探讨;select
函数适用于所有元组。
print((function(...) return select('#', ...) == 1 and "nil" or "no value" end)()) --> no value
print((function(...) return select('#', ...) == 1 and "nil" or "no value" end)(nil)) --> nil
print((function(...) return select('#', ...) == 1 and "nil" or "no value" end)(variable_that_is_not_defined)) --> nil
-- As this code shows, the function is able to detect whether the value nil was passed as an argument or whether there was simply no value passed.
-- In normal circumstances, both are considered as nil, and this is the only way to distinguish them.
- ↑ "Lua 函数 - 综合指南及示例". 2023-03-22. 检索于 2023-05-17.
- ↑ 更多信息,请参阅:Ierusalimschy, Roberto (2003). "应用程序代码中的错误处理". Lua 编程 (第一版). Lua.org. ISBN 8590379817. 检索于 2014年6月20日.