Lua 编程/单页版本
Lua(不是“LUA”,尽管很常见,但这是不正确的)是一种功能强大、快速、轻量级且可嵌入的编程语言。它被许多框架、游戏和其他应用程序使用。虽然它可以独立使用,但它被设计成易于嵌入到另一个应用程序中。它用 ANSI C 实现,ANSI C 是 C 编程语言的一个子集,非常便携,这意味着它可以在许多系统和许多设备上运行,而大多数其他脚本语言无法运行。本书的目的是教授 Lua 编程,无论其之前的编程经验如何。本书可以作为编程的入门,适用于从未编程过的人,也可以作为 Lua 的入门,适用于以前编程过但没有使用 Lua 的人。由于有许多开发平台和游戏使用 Lua,因此本书还可以用于学习 Lua,然后将其用于该开发平台。
本书旨在教授 Lua 最新版本的用法。这意味着将尝试定期更新它,因为 Lua 的新版本发布(Lua 的发布频率足够低,所以这应该不会太难)。目前,本书已经更新到 Lua 5.2。如果您在嵌入式环境中使用 Lua,该环境使用 5.x 分支(Lua 5.0 和 Lua 5.1)中的旧版本 Lua,那么这些材料对于您来说仍然足够相关。
Lua 由位于巴西的里约热内卢天主教大学设计和维护。它的创建者是 Roberto Ierusalimschy、Waldemar Celes 和 Luiz Henrique de Figueiredo。
“Lua”(发音为 LOO-ah)在葡萄牙语中是“月亮”的意思。因此,它既不是缩写也不是缩写,而是一个名词。更具体地说,“Lua”是一个名字,地球的月亮的名字以及语言的名字。像大多数名字一样,它应该用小写字母写,开头大写,即“Lua”。请不要写成“LUA”,这既难看又令人困惑,因为那样的话,它就会成为不同人有不同含义的缩写词。所以,请写“Lua”吧!——Lua 作者, 关于 Lua
Lua 来自 TeCGraf(里约热内卢天主教大学的一个实验室)设计的两种语言:DEL 和 Sol。DEL 代表“数据输入语言”,而 Sol 代表“简单对象语言”,在葡萄牙语中也代表太阳,因此选择了 Lua 这个名字,因为它在葡萄牙语中是“月亮”的意思。它是为巴西石油公司 Petrobras 创建的,但也用于 TeCGraf 中的许多其他项目,现在已被全球范围内的众多项目使用。Lua 是嵌入式游戏开发领域领先的语言之一。
Lua 的主要优势之一是它的简单性。一些公司之所以只使用它,就是因为这个优势:他们认为如果员工可以使用编程语言来执行某些任务,他们会做得更好,但他们无法负担为员工提供关于复杂编程语言的完整课程。像 Bash 或 Batch 这样的简单语言不足以执行这些任务,而 Lua 既强大又简单。Lua 的另一个重要优势是它的可嵌入性,这是它在整个开发过程中最重要的特征之一。像魔兽世界或 ROBLOX 这样的游戏需要能够在其应用程序中嵌入 Lua,以便应用程序的用户可以使用它。
编程,在嵌入式应用程序中运行的程序中也被称为脚本,是编写计算机程序的过程。编程语言是一种语言,用于通过计算机程序中的计算机代码向计算机发出指令。编程语言包含两部分:语法,就像英语中的语法一样,以及库,语言提供的基本函数。这些库可以与英语中的词汇相比较。
Lua 可以嵌入到应用程序中,也可以独立使用。本书不会介绍在您的计算机上安装 Lua 的过程,但您可以使用 codepad 或 Lua 演示 执行代码。本书中第一个 Lua 代码示例将是基本且传统的 hello world 程序。
“Hello world”程序是一个计算机程序,它在显示设备上输出“Hello, world”。因为它通常是大多数编程语言中最简单的程序之一,所以按照传统,它通常被用来向初学者说明编程语言最基本的语法,或者验证语言或系统是否正常运行。——维基百科, Hello world program
print("Hello, world!")
上面的代码将文本 Hello, world! 打印到输出,打印是指在输出中显示文本,而不是在纸上打印。它通过使用字符串“Hello, world!”作为参数调用print
函数来实现。这将在关于函数的章节中解释。
请注意,Lua 通常嵌入在更低级的应用程序中,这意味着print
函数并不总是将文本显示在用户可见的区域。这些应用程序的编程接口文档通常会解释如何将文本显示给用户。
注释是编程语言忽略的代码注解。注释可以用来描述一行或多行代码,记录程序,临时禁用代码,或任何其他目的。它们需要以两个连字符为前缀才能被 Lua 识别,可以单独占一行,也可以放在其他行末尾
print("This is normal code.")
-- This is a comment
print("This is still normal code.") -- This is a comment at the end of a line of code.
这些注释被称为简短注释。还可以创建长注释,它们以一个长括号开始,可以延续多行
print("This is normal code")
--[[Line 1
Line 2
]]
长括号由两个括号组成,中间可以放置任意数量的等号。这个数量被称为长括号的级别。长括号将一直延续到下一个相同级别的括号,如果有的话。没有等号的长括号被称为级别为 0 的长括号。这种方法使得可以在长注释内部使用结束双括号成为可能,方法是在两个括号中间添加等号。当使用注释禁用代码块时,这样做通常很有用。
--[==[ This is a comment that contains a closing long bracket of level 0 which is here: ]] However, the closing double bracket doesn't make the comment end, because the comment was opened with an opening long bracket of level 2, and only a closing long bracket of level 2 can close it. ]==]
在上面的示例中,级别为 0 的结束长括号 (]]
) 不会关闭注释,但级别为 2 的结束长括号 (]==]
) 会关闭注释。
编程语言的语法定义了在该编程语言中如何编写语句和表达式,就像语法定义了如何编写句子和单词一样。语句和表达式可以分别与句子和单词进行比较。表达式是具有值的代码片段,可以被求值,而语句是可执行的代码片段,包含指令和一个或多个用于该指令的表达式。例如,3 + 5
是一个表达式,而 variable = 3 + 5
是一个语句,它将 variable 的值设置为该表达式。
Lua 的完整语法可以在 Lua 网站上找到扩展的巴克斯-诺尔范式 上,但是如果你读它,你将无法理解任何东西。 扩展的巴克斯-诺尔范式 是一种元语言,一种用于描述另一种语言的语言,就像元网站是关于网站的网站,就像元表在 Lua 中是定义其他表行为的表一样(你将在本书的后面学习元表和表)。但是你不需要学习本书中的扩展的巴克斯-诺尔范式,因为,虽然像 Lua 这样的语言可以使用元语言来描述,但它也可以使用英文中的单词和句子来描述,这正是本书要做的。
既然英文可以用来描述另一种语言,那么它本身一定是一种元语言(因为它符合元语言的定义)。事实确实如此。既然编程语言的目的是描述指令,而你也可以用英文来做,那么英文也一定是一种编程语言。这,在某种程度上,也是事实。实际上,英语是一种可以用于多种用途的语言。但是扩展的巴克斯-诺尔范式是一种专门的语言,编程语言也是专门的语言。专业化是指擅长做某一特定事情,但不能做其他事情的特点。扩展的巴克斯-诺尔范式非常擅长描述其他语言,但它不能用来编写指令或传达信息。编程语言非常擅长提供指令,但它们不能用来描述语言或传达信息。
英语能够做任何事情:描述语言、提供指令和传达信息。但它并不擅长做其中的一些事情。事实上,它在提供指令方面非常糟糕,以至于如果它被用来向计算机提供指令,计算机将无法理解任何内容。这是因为计算机需要指令非常精确和明确。
Lua 可以从官方 Lua 网站上获得,下载页面。那里也有说明:下载按钮用于获取源代码,这可能不是你想要的。你可能在寻找二进制文件,因此你应该浏览页面以查找关于二进制文件的信息(你究竟要找什么取决于你正在使用的平台)。本书的目的是教授 Lua 语言,而不是教授 Lua 工具的使用。通常假设读者将在嵌入式环境中使用 Lua,但这对于本书有用并不需要如此,只是意味着本书不会描述 Lua 作为独立语言的使用。
如前所述,表达式是具有值的代码片段,可以被求值。它们不能直接执行(函数调用除外),因此,仅包含以下代码的脚本(它包含一个表达式)将是错误的
3 + 5
-- The code above is erroneous because all it contains is an expression.
-- The computer cannot execute '3 + 5', since that does not make sense.
代码必须由一系列语句组成。这些语句可以包含表达式,表达式将是语句必须操作或使用来执行指令的值。
本章中的一些代码示例并不构成有效的代码,因为它们仅包含表达式。在下一章中,将介绍语句,并将能够开始编写有效的代码。
求值一个表达式就是计算它以找到它的值。给定表达式求值的结果可能在不同的上下文中有所不同,因为它可能取决于环境和堆栈级别。这个值有时是一个数字,有时是文本,其他时候是许多其他数据类型中的任何一个,这就是为什么说它具有类型。
在 Lua 中,以及在一般的编程中,表达式通常由一个或多个值和零个或多个运算符组成。某些运算符只能与某些类型一起使用(例如,尝试除以文本是不合逻辑的,而除以数字是有意义的)。运算符有两种:一元运算符和二元运算符。一元运算符是只接受一个值的运算符。例如,一元 - 运算符只接受一个数字作为参数:-5、-3、-6 等。它接受一个数字作为参数,并对该数字取反。但是,二元 - 运算符(它不是同一个运算符)接受两个值,并从第一个值中减去第二个值:5 - 3、8 - 6、4 - 9 等。
可以使用type
函数获取数字的类型作为字符串
print(type(32425)) --> number
数字通常表示数量,但它们可以用于许多其他用途。Lua 中的数字类型的工作方式与实数基本相同。数字可以构建为整数、小数、小数指数,甚至可以构建为 十六进制。以下是一些有效的数字
- 3
- 3.0
- 3.1416
- 314.16e-2
- 0.31416E1
- 0xff
- 0x56
Lua 中的数字运算符如下所示
运算 | 语法 | 描述 | 示例 | 结果 |
---|---|---|---|---|
算术取反 | -a | 改变 a 的符号,并返回该值 | -3.14159 | |
加法 | a + b | 返回 a 和 b 的和 | 5.2 + 3.6 | 8.8 |
减法 | a - b | 从 a 中减去 b 并返回结果 | 6.7 - 1.2 | 5.5 |
乘法 | a * b | 返回 a 和 b 的乘积 | 3.2 * 1.5 | 4.8 |
求幂 | a ^ b | 返回 a 的 b 次方,或 a 的 b 次幂 | 5 ^ 2 | 25 |
除法 | a / b | 用 b 除 a 并返回结果 | 6.4 / 2 | 3.2 |
地板除 | a // b | 用 b 除 a 并返回结果的整数部分 | 6.4 // 2 | 3 |
模运算 | a % b | 返回 a 除以 b 的余数 | 5 % 3 | 2 |
你可能已经知道所有这些运算符(它们与基本的数学运算符相同),除了最后一个。最后一个称为模运算符,它只计算一个数字除以另一个数字的余数。例如,5 % 3 的结果是 2,因为 2 是 5 除以 3 的余数。模运算符不像其他运算符那么常见,但它有很多用途。
整数
[edit | edit source]在 Lua 5.3 中添加了一种新的数字子类型,即整数。数字可以是整数或浮点数。浮点数类似于上面描述的数字,而整数是没有任何小数部分的数字。浮点数除法 (/
) 和求幂总是将其操作数转换为浮点数,而所有其他运算符如果它们的两个操作数是整数,则会给出整数。在其他情况下,除了地板除运算符 (//
) 之外,结果是浮点数。
空值
[edit | edit source]空值是空值类型,它的主要特性是与任何其他值不同;它通常代表没有有用的值。一些空值示例
- 在为变量赋值之前访问变量的值
- 尝试在变量作用域之外访问变量时获得的值
- 表中任何未分配键的值
- 如果
tonumber
无法将字符串转换为数字,则返回的值
在更高级的说明中,故意分配一个空值会删除对变量或表的引用,并允许 垃圾回收器 重新回收其内存。
布尔值
[edit | edit source]布尔值可以是真或假,但不能是其他任何值。这在 Lua 中写为 true
和 false
,它们是保留关键字。重要的是要注意 nil
是如前所述的不同数据类型。and
、or
、not()
通常与布尔值相关联,但在 Lua 中可以与任何数据类型一起使用。
运算 | 语法 | 描述 |
---|---|---|
布尔值否定 | not a | 如果 a 为假或空值,则返回真。否则,返回假。 |
逻辑与 | a and b | 如果第一个参数为假或空值,则返回第一个参数。否则,返回第二个参数。 |
逻辑或 | a or b | 如果第一个参数既不为假也不为空值,则返回第一个参数。否则,返回第二个参数。 |
本质上,not
运算符只是否定布尔值(如果它是真则将其设为假,如果它是假则将其设为真),and
运算符如果两者都为真则返回真,否则返回假,or
运算符如果任何一个参数为真则返回真,否则返回假。然而,这不是它们工作的方式,因为它们工作的确切方式如上表所示。在 Lua 中,值 false 和 nil 在逻辑表达式中都被认为是假,而其他所有东西都被认为是真(即使是 0 和空字符串)。
下一章介绍的关系运算符 (<
、>
、<=
、>=
、~=
、==
) 不一定以布尔值为操作数,但总是会给出布尔值作为结果。
这可能难以协调。为了更清楚起见,这里有一些真值表或表达式-结果对。这里 x
可以是 nil
、true
或 false
表达式 | 结果 |
---|---|
true and x
|
x
|
false and x
|
false
|
nil and x
|
nil
|
true or x
|
true
|
false or x
|
x
|
nil or x
|
x
|
这有点违反直觉地意味着
表达式 | 结果 |
---|---|
false and nil
|
false
|
nil and false
|
nil
|
此外,
表达式 | 结果 |
---|---|
false == nil nil == false
|
false
|
nil and false
|
nil
|
表达式 | 结果 |
---|---|
not(false) not(nil)
|
true
|
not(true)
|
false
|
字符串
[edit | edit source]字符串是字符序列,可用于表示文本。它们可以在 Lua 中通过包含在双引号、单引号或长括号中来编写,这些在前面的 关于注释的部分 中已经介绍过(需要注意的是,注释和字符串除了都可以用长括号分隔外,没有其他共同之处,注释之前是两个连字符)。不包含在长括号中的字符串只会延续一行。因此,要创建一个包含多行字符串而不用长括号的唯一方法是使用转义序列。这也是在某些情况下插入单引号或双引号的唯一方法。转义序列由两部分组成:转义字符,在 Lua 中总是反斜杠 ('\'),以及标识要转义的字符的标识符。
转义序列 | 描述 |
---|---|
\n | 换行符 |
\" | 双引号 |
\' | 单引号(或撇号) |
\\ | 反斜杠 |
\t | 水平制表符 |
\### | ### 必须是 0 到 255 之间的数字。结果将是相应的 ASCII 字符。 |
当直接将字符放在字符串中会导致问题时,会使用转义序列。例如,如果你有一个用双引号括起来的文本字符串,并且必须包含双引号,那么你需要用不同的字符括起来该字符串,或者转义双引号。在用长括号分隔的字符串中转义字符是不必要的,所有字符都是这样。用长括号分隔的字符串中的所有字符都将按原样使用。%
字符在字符串模式中用于转义魔法字符,但术语转义是在另一个上下文中使用的。
"This is a valid string."
'This is also a valid string.'
"This is a valid \" string 'that contains unescaped single quotes and escaped double quotes."
[[
This is a line that can continue
on more than one line.
It can contain single quotes, double quotes and everything else (-- including comments). It ignores everything (including escape characters) except closing long brackets of the same level as the opening long bracket.
]]
"This is a valid string that contains tabs \t, double quotes \" and backlashes \\"
"This is " not a valid string because there is an unescaped double quote in the middle of it."
为了方便起见,如果一个开头的长字符串括号紧跟着一个换行符,那么该换行符将被忽略。因此,以下两个字符串是等效的
[[This is a string
that can continue on many lines.]]
[[
This is a string
that can continue on many lines.]]
-- Since the opening long bracket of the second string is immediately followed by a new line, that new line is ignored.
可以使用一元长度运算符 ('#') 获取字符串的长度,它是一个数字
print(#("This is a string")) --> 16
连接
[edit | edit source]— 维基百科, 连接
Lua 中的字符串连接运算符用两个点 ('..') 表示。以下是一个将 “snow” 和 “ball” 连接起来并打印结果的连接示例
print("snow" .. "ball") --> snowball
此代码将连接 “snow” 和 “ball” 并在输出窗口打印结果。
其他类型
[edit | edit source]Lua 中的四种基本类型(数字、布尔值、空值和字符串)已在前面各节中介绍,但还缺少四种类型:函数、表、用户数据和线程。 函数 是可以调用的代码块,可以接收值并返回值。 表 是用于数据操作的数据结构。 用户数据 由嵌入 Lua 的应用程序内部使用,以允许 Lua 通过应用程序控制的对象与该程序通信。最后,线程 由协程使用,协程允许许多函数同时运行。这些将在后面描述,所以你只需要记住还有其他数据类型。
字面量
[edit | edit source]字面量是在源代码中表示固定值的表示法。除了线程和用户数据之外,所有值都可以在 Lua 中表示为字面量。例如,字符串字面量(计算结果为字符串的字面量)由必须表示的字符串文本组成,该文本包含在单引号、双引号或长括号中。另一方面,数字字面量由它们代表的数字组成,这些数字使用十进制表示法(例如:12.43
)、科学记数法(例如:3.1416e-2
和 0.31416E1
)或十六进制表示法(例如:0xff
)表示。
强制转换
[edit | edit source]强制转换是将一个数据类型的值转换为另一个数据类型的值。Lua 在字符串和数字值之间提供自动强制转换。对字符串应用的任何算术运算都将尝试将该字符串转换为数字。相反,当需要字符串而使用数字时,数字将被转换为字符串。这适用于 Lua 运算符和默认函数(随语言提供的函数)。
print("122" + 1) --> 123
print("The number is " .. 5 .. ".") --> The number is 5.
数字到字符串和字符串到数字的强制转换也可以使用tostring
和tonumber
函数手动完成。前者接受一个数字作为参数并将其转换为字符串,而后者接受一个字符串作为参数并将其转换为数字(可选地在第二个参数中给出与默认十进制不同的基数)。
位运算
[edit | edit source]从 Lua 5.3 开始,提供位运算符来对二进制数(位模式)进行运算。这些运算符不像其他运算符那样常用,因此,如果您不需要它们,可以跳过本节。
Lua 中的位运算符始终对整数进行运算,如果需要,会将其操作数转换为整数。它们也返回整数。
按位与运算(运算符为&
)对两个等长二进制表示的每对位执行逻辑与运算。例如,5 & 3
计算结果为 1。我们可以通过查看这些数字的二进制表示来解释这一点(下标用于表示基数)
如果 5 和 3 的二进制表示中给定位置的位都是 1(最后一个位的情况),那么结果中该位置的位将为 1;在所有其他情况下,它将为 0。
按位或运算(运算符为|
)的工作方式与按位与相同,执行逻辑或运算,而不是执行逻辑与运算。因此,5 | 3
将计算结果为 7
在这里,我们可以看到,最终结果中每个位置的位仅当两个操作数的二进制表示在该位置具有 0 位时才为 0。
按位异或运算(运算符为~
)的工作方式与其他两个运算符类似,但在给定位置,最终位仅当操作数中的一个位(而不是两个)为 1 时才为 1。
这与前面的示例相同,但我们可以看到,结果中的最后一个位为 0 而不是 1,因为两个操作数的最后一个位都是 1。
按位取反运算(运算符为~
)对唯一操作数的每个位执行逻辑取反运算,这意味着每个 0 变成 1,每个 1 变成 0。因此,~7
将计算结果为 -8
在这里,第一个位在结果中变成了 1,因为它在操作数中是 0,其他位变成了 0,因为它们都是 1。
除了这些位运算符之外,Lua 5.3 还支持算术位移。 左移(运算符为<<
,左侧图示)包括将所有位向左移动,移动的位数与第二个操作数对应。 右移(运算符为>>
,右侧图示)执行相同的操作,但方向相反。
运算符优先级
[edit | edit source]运算符优先级在 Lua 中的工作方式与数学中通常相同。某些运算符将在其他运算符之前进行计算,并且可以使用括号来任意更改操作应执行的顺序。运算符计算的优先级在下面的列表中,从优先级高到低。其中一些运算符尚未讨论,但它们都将在本书的某个部分介绍。
- 指数运算:
^
- 一元运算符:
not
、#
、-
、~
- 级别 2 数学运算符:
*
、/
、//
、%
- 级别 1 数学运算符:
+
、-
- 连接:
..
- 位移:
<<
、>>
- 按位与:
&
- 按位异或:
~
- 按位或:
|
- 关系运算符:
<
、>
、<=
、>=
、~=
、==
- 布尔与:
and
- 布尔或:
or
测验
[edit | edit source]你可以回答一些问题,以验证你是否理解了本章的内容。请注意,要找到其中一些问题的答案,可能需要你具备本章未介绍的知识。这是正常的:测验是学习体验的一部分,它们可以介绍本书其他地方没有介绍的信息。
语句
[edit | edit source]语句是可以执行的代码片段,包含指令和用于该指令的表达式。一些语句还将在其内部包含代码,例如,这些代码可能会在特定条件下运行。与表达式不同,它们可以直接放在代码中并执行。Lua 的指令很少,但这些指令与其他指令以及复杂的表达式结合使用,可以为用户提供大量的控制和灵活性。
赋值
[edit | edit source]程序员经常需要能够将值存储在内存中以便以后使用它们。这是使用变量来完成的。 变量是存储在计算机内存中的值的引用。它们可以用来在将数字存储在内存中之后访问该数字。 赋值是用于将值分配给变量的指令。它由应存储值的变量名称、一个等号以及应存储在变量中的值组成
variable = 43
print(variable) --> 43
如上面的代码所示,可以通过将变量名称放在应该访问值的 地方来访问变量的值。
在 Lua 中,与大多数其他编程语言一样,等号 (=
) 充当二元 赋值运算符,将右手操作数表达式的值赋给左手操作数命名的变量。
以下示例展示了使用等号进行变量赋值。
fruit = "apple" -- assign a string to a variable
count = 5 -- assign a numeric value to a variable
请注意,字面量字符串应该用引号括起来,以将其与变量名区分开来。
apples = 5
favourite = "apples" -- without quotes, apples would be interpreted as a variable name
请注意,数值不需要用引号括起来,也不能被误解为变量名,因为变量名不能以数字开头。
apples = 6 -- no quotes are necessary around a numeric parameter
pears = "5" -- quotes will cause the value to be considered a string
Lua 编程语言支持多重赋值。
apples, favorite = 5, "apples" -- assigns apples = 5, favorite = "apples"
标识符,在 Lua 中,也称为名称。它们可以是任何由字母、数字和下划线组成的文本,并且不能以数字开头。它们用于命名变量和表格字段,这些将在关于表格的章节中介绍。
以下是一些有效的名称
name
hello
_
_tomatoes
me41
__
_thisIs_StillaValid23name
以下是一些无效的名称
2hello
: 以数字开头th$i
: 包含非字母、数字或下划线的字符hel!o
: 包含非字母、数字或下划线的字符563text
: 以数字开头82_something
: 以数字开头
此外,以下关键字由 Lua 保留,不能用作名称:and
, break
, do
, else
, elseif
, end
, false
, for
, function
, if
, in
, local
, nil
, not
, or
, repeat
, return
, then
, true
, until
, while
.
在命名变量或表格字段时,必须为其选择有效的名称。因此,它必须以字母或下划线开头,并且只能包含字母、下划线和数字。请注意,Lua 区分大小写。这意味着 Hello
和 hello
是两个不同的名称。
变量的作用域 是程序代码中该变量有意义的区域。您之前看到的变量示例都是全局变量的示例,这些变量可以在程序的任何地方访问。另一方面,局部变量只能在定义它们的程序区域以及位于该程序区域内的程序区域中使用。它们以与全局变量完全相同的方式创建,但必须以 local
关键字为前缀。
do
语句将用于描述它们。do
语句是一个没有其他目的的语句,只是为了创建一个新的代码块,因此创建一个新的作用域。它以 end
关键字结束。
local variable = 13 -- This defines a local variable that can be accessed from anywhere in the script since it was defined in the main region.
do
-- This statement creates a new block and also a new scope.
variable = variable + 5 -- This adds 5 to the variable, which now equals 18.
local variable = 17 -- This creates a variable with the same name as the previous variable, but this one is local to the scope created by the do statement.
variable = variable - 1 -- This subtracts 1 from the local variable, which now equals 16.
print(variable) --> 16
end
print(variable) --> 18
当作用域结束时,其中的所有变量都会被清除。代码区域可以使用定义在其包含的代码区域中的变量,但如果它们通过定义具有相同名称的局部变量来“覆盖”它们,则将使用该局部变量,而不是定义在其他代码区域中的变量。这就是为什么第一次调用 print 函数打印 16,而第二次调用在 do
语句创建的作用域之外,打印 18。
实际上,应该只使用局部变量,因为它们可以比全局变量更快地定义和访问,因为它们存储在寄存器中,而不是存储在当前函数的环境中,就像全局变量一样。寄存器是 Lua 用于存储局部变量以快速访问它们的区域,通常最多只能包含 200 个局部变量。处理器是所有计算机的重要组成部分,它也有寄存器,但这些与 Lua 的寄存器无关。每个函数(包括主线程,程序的核心,它也是一个函数)也有自己的环境,它是一个使用索引作为变量名称并将这些变量的值存储在对应于这些索引的值中的表格。
增量赋值,也称为 复合赋值,是一种赋值类型,它赋予变量一个与其先前值相关的值,例如,递增当前值。a += 8
代码的等效项,它将 a 的值递增 8,已知存在于 C、JavaScript、Ruby、Python 中,在 Lua 中不存在,这意味着有必要编写 a = a + 8
。
链式赋值 是一种赋值类型,它赋予多个变量一个单一值。例如,代码 a = b = c = d = 0
会在 C 和 Python 中将 a、b、c 和 d 的值设置为 0。在 Lua 中,此代码会引发错误,因此有必要像这样编写前面的示例。
d = 0
c = d -- or c = 0
b = c -- or b = 0
a = b -- or a = 0
并行赋值,也称为 同时赋值 和 多重赋值,是一种同时将不同的值(它们也可以是相同的值)赋予不同变量的赋值类型。与链式赋值和增量赋值不同,并行赋值在 Lua 中是可用的。
上一节中的示例可以改写为使用并行赋值。
a, b, c, d = 0, 0, 0, 0
如果您提供的变量比值多,则某些变量将不会被分配任何值。如果您提供的值比变量多,则多余的值将被忽略。更准确地说,在分配发生之前,值的列表将根据变量列表的长度进行调整,这意味着多余的值将被删除,并且将在其末尾添加额外的 nil 值,使其长度与变量列表相同。如果函数调用存在于 值的列表末尾,它返回的值将被添加到该列表的末尾,除非函数调用放在括号中。
此外,与大多数编程语言不同,Lua 允许通过 置换 重新分配变量的值。例如
first_variable, second_variable = 54, 87
first_variable, second_variable = second_variable, first_variable
print(first_variable, second_variable) --> 87 54
这是因为赋值语句在分配任何内容之前评估所有变量和值。赋值就像它们是真正同时执行的一样,这意味着您可以同时将值分配给变量和分配给使用该变量的值作为索引的表格字段,在它被分配一个新值之前。换句话说,以下代码将把 dictionary[2]
设置为 12,而不是 dictionary[1]
。
dictionary = {}
index = 2
index, dictionary[index] = index - 1, 12
条件语句是检查表达式是否为真的指令,如果为真则执行特定的代码段。如果表达式不为真,它们只会跳过该代码段,程序继续执行。在 Lua 中,唯一的条件语句使用 if
指令。False 和 nil 都被认为是 false,而其他所有内容都被认为是 true。
local number = 6
if number < 10 then
print("The number " .. number .. " is smaller than ten.")
end
-- Other code can be here and it will execute regardless of whether the code in the conditional statement executed.
在上面的代码中,变量 number 使用赋值语句被分配了数字 6。然后,条件语句检查存储在变量 number 中的值是否小于十,这里的情况就是这样。如果是,则打印 "数字 6 小于十。"。
还可以使用 else
关键字来仅在表达式不为真的情况下执行特定的代码段,并使用 elseif
关键字来链接条件语句。
local number = 15
if number < 10 then
print("The number is smaller than ten.")
elseif number < 100 then
print("The number is bigger than or equal to ten, but smaller than one hundred.")
elseif number ~= 1000 and number < 3000 then
print("The number is bigger than or equal to one hundred, smaller than three thousands and is not exactly one thousand.")
else
print("The number is either 1000 or bigger than 2999.")
end
请注意,else
块必须始终是最后一个块。在 else
块之后不能有 elseif
块。只有在前面所有块都没有执行的情况下,elseif
块才有意义。
用于比较两个值的运算符称为关系运算符,其中一些在上面的代码中使用。如果关系为真,则它们返回布尔值 true
。否则,它们返回布尔值 false
。
等于 | 不等于 | 大于 | 小于 | 大于或等于 | 小于或等于 | |
---|---|---|---|---|---|---|
数学符号 | = | ≠ | > | < | ≥ | ≤ |
Lua 运算符 | == | ~= | > | < | >= | <= |
上面的代码还演示了如何使用 and
关键字在条件表达式中组合多个布尔表达式。
程序员经常需要多次运行一段特定的代码或类似的代码,或者需要运行一段特定代码的次数,而该次数可能取决于用户输入。循环是一组语句,这些语句只指定一次,但可以连续执行多次。
条件控制循环是由条件控制的循环。它们与条件语句非常相似,但它们不会在条件为真时执行代码,而在条件为假时跳过代码,而是会持续运行代码,直到条件为真,或者直到条件为假。Lua 有两个用于条件控制循环的语句:while
循环和 repeat
循环。此类循环将运行代码,然后检查条件是否为真。如果为真,则它们再次运行代码,并重复,直到条件为假。当条件为假时,它们停止重复代码,程序流程继续。代码的每次执行称为一次迭代。while
循环和 repeat
循环之间的区别在于,repeat
循环将在循环结束时检查条件,而 while
循环将在循环开始时检查条件。这仅对第一次迭代有所区别:repeat
循环始终至少执行一次代码,即使在第一次执行代码时条件为假。而 while
循环则不是这样,它仅在条件实际为真时才第一次执行代码。
local number = 0
while number < 10 do
print(number)
number = number + 1 -- Increase the value of the number by one.
end
上面的代码将打印 0,然后打印 1,然后打印 2,然后打印 3,依此类推,直到打印 9。在第十次迭代之后,number 将不再小于十,因此循环将停止执行。有时,循环旨在永远运行,在这种情况下,它们被称为无限循环。渲染器(例如,在屏幕上绘制内容的软件进程)通常会不断循环以重新绘制屏幕,以更新显示给用户的图像。这在视频游戏中很常见,因为游戏视图必须不断更新,以确保用户看到的内容保持最新状态。但是,需要循环永远运行的情况很少见,此类循环通常是错误的结果。无限循环会占用大量的计算机资源,因此务必确保循环始终结束,即使用户收到意外的输入。
local number = 0
repeat
print(number)
number = number + 1
until number >= 10
上面的代码将与使用 while
循环的代码执行完全相同的事情。主要区别在于,与 while
循环不同,while
循环将条件放在 while
关键字和 do
关键字之间,而 repeat
循环将条件放在循环的末尾,在 until
关键字之后。repeat
循环是 Lua 中唯一创建一个块且不以 end
关键字结尾的语句。
递增变量是将其值增加步长,特别是增加一步长。上一节中的两个循环递增了变量 number 并使用它来运行代码一定的次数。这种循环非常常见,以至于大多数语言(包括 Lua)都为此内置了控制结构。这种控制结构称为计数控制循环,在 Lua 和大多数语言中,由 for
语句定义。在这些循环中使用的变量称为循环计数器。
for number = 0, 9, 1 do
print(number)
end
上面的代码与上一节中介绍的两个循环执行完全相同的事情,但 number 变量只能在循环内部访问,因为它对循环是局部的。变量名和等号后面的第一个数字是初始化。它是循环计数器初始化的值。第二个数字是循环停止的数字。它将递增变量并重复代码,直到变量达到此数字。最后,第三个数字是增量:它是每次迭代时循环计数器增加的值。如果没有给出增量,Lua 将假定它为 1。因此,下面的代码将打印 1、1.1、1.2、1.3、1.4 和 1.5。
for n = 1, 2, 0.1 do
print(n)
if n >= 1.5 then
break -- Terminate the loop instantly and do not repeat.
end
end
上面的代码没有一直上升到 2,而是只上升到 1.5 的原因是 break
语句,它会立即终止循环。此语句可以与任何循环一起使用,包括 while
循环和 repeat
循环。请注意,这里使用了 >=
运算符,尽管理论上 ==
运算符也可以完成任务。这是因为十进制精度误差。Lua 使用 双精度浮点数格式 来表示数字,该格式将数字存储在内存中,作为其实际值的近似值。在某些情况下,近似值将与数字完全匹配,但在某些情况下,它将仅是近似值。通常,这些近似值足够接近数字,以至于不会产生影响,但此系统可能会在使用等号运算符时导致错误。这就是为什么在处理十进制数时,通常更安全地避免使用等号运算符。在这个特定情况下,如果使用了等号运算符,代码将无法工作[1](它会继续上升到 1.9),但使用 >=
运算符就可以工作。
块是按顺序执行的语句列表。这些语句可以包括空语句,这些语句不包含任何指令。空语句可以用于以分号开始一个块,或者在序列中写入两个分号。
函数调用和赋值可以以括号开头,这会导致歧义。以下片段就是一个例子
a = b + c
(print or io.write)('done')
此代码可以有两种解释
a = b + c(print or io.write)('done')
a = b + c; (print or io.write)('done')
当前解析器始终以第一种方式查看这些结构,将左括号解释为调用参数的开头。为了避免这种歧义,最好始终以分号作为以括号开头的语句的前缀
;(print or io.write)('done')
Lua 的编译单元称为 块。块可以存储在文件中,也可以存储在宿主程序中的字符串中。要执行一个块,Lua 首先将其预编译为虚拟机的指令,然后使用虚拟机的解释器执行编译后的代码。块也可以使用 luac
(随 Lua 提供的编译程序)或 string.dump
函数(它返回包含给定函数的二进制表示的字符串)预编译为二进制形式(字节码)。
load
函数可以用来加载一个块。如果给 load
函数的第一个参数是一个字符串,则该块就是该字符串。在这种情况下,该字符串可以是 Lua 代码或 Lua 字节码。如果第一个参数是一个函数,则 load
将重复调用该函数以获取块的片段,每个片段都是一个字符串,将与之前的字符串连接起来。然后,当什么也不返回或返回空字符串时,就认为该块已完成。
如果 load
函数没有语法错误,则它将返回已编译的块作为函数。否则,它将返回 nil 和错误消息。
load
函数的第二个参数用于设置代码块的来源。所有代码块都保留其来源的副本,以便能够给出适当的错误消息和调试信息。默认情况下,其来源的副本将是传递给 load
的代码(如果提供了代码;如果改为提供函数,它将是“=(load)”。)。可以使用此参数来更改它。这在编译代码时非常有用,可以防止人们获取原始代码。然后有必要删除包含在二进制表示中的源代码,否则可以在那里获取原始代码。
load
函数的第三个参数可以用于设置生成的函数的环境,第四个参数控制代码块可以是文本还是二进制。它可以是字符串“b”(仅二进制代码块)、“t”(仅文本代码块)或“bt”(二进制代码块和文本代码块)。默认值为“bt”。
还有一个 loadfile
函数,它的工作方式与 load
完全相同,但它从文件中获取代码。第一个参数是从中获取代码的文件名。没有参数可以修改存储在二进制表示中的源代码,load
函数的第三个和第四个参数分别对应于此函数的第二个和第三个参数。loadfile
函数也可以用于从标准输入加载代码,如果没有给出文件名,则将执行此操作。
dofile
函数类似于 loadfile
函数,但它不是将文件中的代码加载为函数,而是立即将源代码文件中的代码作为 Lua 代码块执行。它的唯一参数用于指定应执行其内容的文件名;如果没有给出参数,它将执行标准输入的内容。如果代码块返回值,它们将由对 dofile
函数的调用提供。由于 dofile
不在受保护模式下运行,因此通过它执行的代码块中的所有错误都会传播。
函数
[edit | edit source]一个 堆栈 是一个项目列表,其中可以添加(push)或删除(pop)项目,它遵循后进先出原则,这意味着最后一个添加的项目将是第一个被删除的项目。这就是这种列表被称为堆栈的原因:在堆栈上,您不能在先删除位于其顶部的项目之前删除一个项目。因此,所有操作都在堆栈的顶部进行。如果一个项目是在另一个项目之后添加的,则它位于另一个项目之上;如果是在另一个项目之前添加的,则位于另一个项目之下。
一个 函数(也称为子程序、过程、例程或子程序)是一系列执行特定任务的指令,这些指令可以从程序中的其他地方调用,只要应该执行该指令序列即可。函数还可以接收值作为输入并在可能对输入进行操作或根据输入执行任务后返回输出。函数可以从程序中的任何地方定义,包括在其他函数内部,也可以从程序中访问它们的任何部分调用:函数与数字和字符串一样,都是值,因此可以存储在变量中并具有所有变量的共同属性。这些特点使得函数非常有用。
因为函数可以从其他函数调用,所以 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 值将转换为相应的字符串。
在实践中,术语参数和参数通常可以互换使用。在这本书中,以及在它们适当的含义中,术语参数和参数分别表示一个名称,对应参数的值将被分配给它,以及一个值,它将传递给函数以分配给参数。
返回值
[edit | edit source]函数可以接收输入,对其进行操作并返回输出。您已经知道它们如何接收输入(参数)并对其进行操作(函数体)。它们还可以通过返回一个或多个任何类型的返回值来给出输出,这是使用 return 语句完成的。这就是为什么函数调用既是语句又是表达式的原因:它们可以被执行,但它们也可以被评估。
local function add(first_number, second_number)
return first_number + second_number
end
print(add(5, 6))
上面函数中的代码将首先定义函数 add
。然后,它将使用 5 和 6 作为值调用它。函数将它们相加并返回结果,然后该结果将被打印。这就是为什么上面的代码会打印 11。函数也可以通过用逗号分隔评估为这些值的表达式来返回多个值。
用户可以定义自己的函数,并根据自己的需要对其进行定制。
除了接受参数和返回值外,用户定义的函数还可以具有副作用,即由函数执行引起的变量或程序状态的更改。 [1]
错误
[edit | edit source]错误有三种类型:语法错误、静态语义错误和语义错误。语法错误发生在代码明显无效时。例如,下面的代码将被 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 相同的值,而是将它们作为参数调用处理程序函数。然后,处理程序函数可以用来例如显示错误消息。对于pcall 函数,可以通过传递给xpcall 函数来将参数传递给函数。 |
调用堆栈(包含所有已调用函数的堆栈,按调用顺序排列)前面已提到过。大多数语言(包括 Lua)中的调用堆栈都有最大尺寸。这个最大尺寸非常大,在大多数情况下应该不用担心,但是,如果没有任何东西可以阻止它们无限地反复调用自身,那么调用自身的函数(这被称为递归,这样的函数被称为递归函数)可以达到这个限制。这被称为堆栈溢出。当堆栈溢出时,代码停止运行并抛出错误。
可变参数函数(也称为 vararg 函数)是接受可变数量参数的函数。可变参数函数在其参数列表的末尾用三个点 ("...") 表示。不适合参数列表中参数的参数不会被丢弃,而是通过 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 Functions - Comprehensive Guide with Examples". 2023-03-22. Retrieved 2023-05-17.
- ↑ 有关详细信息,请参见:Ierusalimschy, Roberto (2003). "Error Handling in Application Code". Programming in Lua (first ed.). Lua.org. ISBN 8590379817. Retrieved June 20, 2014.
表是 Lua 中唯一的数据结构,但它们比许多其他语言中的数据结构更灵活。它们也被称为字典(因为它们使值对应于索引,就像字典中的定义对应于词条一样)、关联数组(因为它们将值与索引关联,从而使它们成为与索引关联的值数组)、哈希表、符号表、哈希映射和映射。它们是使用表构造器创建的,表构造器由两个大括号定义,可以选择包含用逗号或分号分隔的值。以下示例演示了数组(一个有序的值列表)并演示了如何获取数组的长度。
local array = {5, "text", {"more text", 463, "even more text"}, "more text"}
print(#array) --> 4 ; the # operator for strings can also be used for arrays, in which case it will return the number of values in the array
-- This example demonstrates how tables can be nested in other tables. Since tables themselves are values, tables can include other tables. Arrays of tables are called multi-dimensional arrays.
表可以包含除 nil 之外的任何类型的值。这是合理的:nil 代表值的缺失。在表中插入“值的缺失”是没有意义的。表中的值可以用逗号或分号分隔,并且可以跨多行继续。通常使用逗号来进行单行表构造器,使用分号来进行多行表,但这并不是必须的。
local array = {
"text";
{
"more text, in a nested table";
1432
};
true -- booleans can also be in tables
}
表由字段组成,字段是值对,其中一个值是索引(也称为键),另一个值是对应于该索引的值。在数组中,索引始终是数值。在字典中,索引可以是任何值。
local dictionary = {
"text";
text = "more text";
654;
[8] = {}; -- an empty table
func = function() print("tables can even contain functions!") end;
["1213"] = 1213
}
如上例所示,可以像在数组中一样向字典添加值,只需添加值本身(在这种情况下,索引将是一个数字),或使用标识符和等号作为前缀添加值(在这种情况下,索引将是与标识符相对应的字符串),或者使用括号括住的值和等号作为前缀添加值(在这种情况下,索引是括号中的值)。后一种方法是使索引与值相对应的最灵活的方式,因为它可以与任何值或表达式一起使用。
可以通过将与值相对应的索引放在方括号中,并在表达式后添加一个计算结果为表的表达式来访问表中的值。
local dictionary = {number = 6}
print(dictionary["number"]) --> 6
如果索引是一个字符串,并且符合 Lua 标识符的标准(不包含空格,不以数字开头,不包含除数字、字母和下划线之外的任何其他字符),则可以通过在字符串前面添加点来访问它,而不使用方括号。
local dictionary = {["number"] = 6}
print(dictionary.number) --> 6
这两个示例创建了相同的表并打印了相同的值,但使用不同的符号定义和访问索引。同样,对于包含其他表的表,也可以先对第一个表进行索引以获取嵌套表,然后对嵌套表中的值进行索引来访问嵌套表中的值。
local nested_table = {
[6] = {
func = function() print("6") end
}
}
nested_table[6].func() -- This will access the nested table, access the function that is inside it and then call that function, which will print the number 6.
关于语句的章节描述了两种类型的循环:条件控制循环和计数控制循环。在 Lua 中,存在第三种类型的循环,即 foreach 循环,也称为泛型 for 循环。 foreach 循环允许程序员为表中的每个字段执行代码。下面的示例演示了一个 foreach 循环,该循环遍历数字数组中的项目,并打印所有索引以及与它们对应的值为 1 的值的总和。
local array = {5, 2, 6, 3, 6}
for index, value in next, array do
print(index .. ": " .. value + 1)
end
-- Output:
-- 1: 6
-- 2: 3
-- 3: 7
-- 4: 4
-- 5: 7
以下示例中的两个循环将执行与上一个示例中的循环相同的操作。
local array = {5, 2, 6, 3, 6}
for index, value in pairs(array) do
print(index .. ": " .. value + 1)
end
for index, value in ipairs(array) do
print(index .. ": " .. value + 1)
end
第一个示例中显示的方法与上一个示例中的第一个循环执行相同的操作。但是,最后一个(使用 ipairs
函数的循环)只会在表中不存在第一个整数键之前迭代,这意味着它只会在数字数组中按顺序迭代。以前有两个函数分别称为 table.foreach
和 table.foreachi
,但它们在 Lua 5.1 中已被弃用,并在 Lua 5.2 中被删除。 弃用 是应用于一项功能或实践的状态,表示它已被删除或取代,应避免使用。因此,使用 foreach 循环遍历表比使用这两个函数更可取。
在 Lua 中,元组(一个简单的值列表)和表(一个将索引映射到值的结构)之间存在区别。调用返回多个值的函数的函数调用将计算为元组。赋值语句中的一系列值,其中多个变量被一次性赋值,也是元组。用于可变参数函数的 vararg 表达式也是元组。由于元组是值列表,因此不是单个值,因此它不能存储在变量中,尽管它可以存储在多个变量中。可以通过将计算为元组的表达式放在表构造函数中来将元组转换为数组。
function return_tuple()
return 5, 6 -- Because this function returns two values, a function call to it evaluates to a tuple.
end
local array = {return_tuple()} -- same as local array = {5, 6}
local a, b = return_tuple() -- same as local a, b = 5, 6
print(return_tuple()) -- same as print(5, 6)
可以通过使用 table 库的 unpack
函数并将数组作为参数来解包数组(将其从表更改为元组)。
local array = {7, 4, 2}
local a, b, c = table.unpack(array)
print(a, b, c) --> 7, 4, 2
在 Lua 5.2 之前的 Lua 版本中,unpack
函数是基本库的一部分。它现在已移至 table 库。
由于表可以包含函数并将名称与这些函数关联,因此它们通常用于创建库。Lua 还具有语法糖,可用于创建方法,这些方法是用于操作对象的函数,通常由表表示。对于不了解面向对象编程的人来说,这可能有点难以理解,面向对象编程超出了本书的范围,因此不理解这一点也无妨。以下两个示例执行完全相同的操作。
local object = {}
function object.func(self, arg1, arg2)
print(arg1, arg2)
end
object.func(object, 1, 2) --> 1, 2
local object = {}
function object:func(arg1, arg2)
print(arg1, arg2)
end
object:func(1, 2) --> 1, 2
当调用表中某个函数时,该函数对应于一个字符串类型的索引,使用冒号而不是点将添加一个隐藏参数,即表本身。类似地,使用冒号而不是点在表中定义函数将在参数列表中添加一个隐藏的 self
参数。使用冒号定义函数并不意味着调用函数时也必须使用冒号,反之亦然,因为它们是完全可以互换的。
在 Lua 中排序表可能比较简单。在大多数情况下,可以将 table 库的 sort
函数用于排序表,这使得一切变得相对容易。sort
函数按指定顺序对数组中的元素进行排序,就地进行(即,不创建新数组)。如果提供了一个函数作为第二个参数,则该函数应接收数组中的两个元素,并在第一个元素应在最终顺序中排在第二个元素之前时返回 true。如果没有提供第二个参数,Lua 将根据 <
运算符对数组中的元素进行排序,该运算符在用于两个数字时返回 true,并且第一个数字小于第二个数字,但也适用于字符串,在这种情况下,它将在第一个字符串按字典顺序小于第二个字符串时返回 true。
元表是表,可用于控制其他表的行为。这通过 元方法 完成,该表中的字段指示 Lua 虚拟机在代码尝试以特定方式操作表时应执行的操作。元方法由其名称定义。例如,__index
元方法告诉 Lua 在代码尝试对表中尚未存在的字段进行索引时该怎么办。可以使用 setmetatable
函数设置表的元表,该函数接受两个表作为参数,第一个表是应设置元表的表,第二个表是应设置表元表的元表。还有一个 getmetatable
函数,它返回表的元表。以下代码演示了如何使用元表使表中所有不存在的字段看起来像包含一个数字;这些数字是使用 math.random
函数随机生成的。
local associative_array = {
defined_field = "this field is defined"
}
setmetatable(associative_array, {
__index = function(self, index)
return math.random(10)
end
})
在上面的示例中,可能会注意到很多事情。其中一件事可能是,包含 __index
元方法的字段的名称以两个下划线为前缀。这种情况总是如此:当 Lua 在表的元表中查找元方法时,它会查找与元方法名称相对应并且以两个下划线开头的索引。另一个可能会注意到的是,__index
元方法实际上是一个函数(大多数元方法是函数,但并非全部都是函数),它接受两个参数。第一个参数 self
是调用 __index
元方法的表。在本例中,我们可以直接引用 associative_array
变量,但这在多个表使用单个元表时很有用。第二个参数是尝试索引的索引。最后,可以注意到该函数通过返回值来告诉 Lua 虚拟机应该向索引表的代码提供什么。大多数元方法只能是函数,但 __index
元方法也可以是一个表。如果是表,当程序尝试索引表中不存在的字段时,Lua 将在该表中查找相同的索引。如果找到该索引,它将返回 __index
元方法中指定的表中与该索引相对应的值。
- __newindex(self, index, value)
__newindex
元方法可用于告诉 Lua 虚拟机在程序尝试在表中添加新字段时该怎么办。它只能是一个函数,并且仅在要写入的索引不存在现有字段时才会被调用。它有三个参数:前两个参数与__index
元方法的参数相同,第三个参数是程序尝试将字段的值设置为的值。- __concat(self, value)
__concat
元方法可用于告诉 Lua 虚拟机在程序尝试使用连接运算符 (..
) 将值连接到表时该怎么办。- __call(self, ...)
__call
元方法可用于指定程序尝试调用表时应该发生什么。这使得表确实可以像函数一样工作。在表被调用时传递的参数将放在self
参数之后,self
参数始终是表本身。此元方法可用于返回值,在这种情况下,调用表将返回这些值。- __unm(self)
- 此元方法可用于指定对表使用一元减运算符的效果。
- __tostring(self)
- 此元方法可以是一个函数,该函数返回
tostring
函数在使用表作为参数调用时应返回的值。 - __add(self, value)
- __sub(self, value)
- __mul(self, value)
- __div(self, value)
- __mod(self, value)
- __pow(self, value)
- 这些元方法可用于分别告诉虚拟机在将值加到、减去、乘以或除以表时该怎么办。最后两个类似,但分别用于模运算符 (
%
) 和幂运算符 (^
)。 - __eq(self, value)
- 此元方法由等号运算符 (
==
) 使用。仅当比较的两个值的元表相同时,才会使用它。不等运算符 (~=
) 使用此函数结果的反面。 - __lt(self, value)
- __le(self, value)
- 这些元方法由“小于”和“小于或等于”运算符使用。“大于”和“大于或等于”运算符将返回这些元方法返回结果的反面。仅当值的元表相同时,才会使用它们。
- __gc(self)
- 当垃圾收集器收集一个附加了此元方法的元表的某个值之前,Lua 会调用此元方法。它只对用户数据值起作用,不能用于表格。
- __len(self)
- 当在用户数据值上使用长度运算符(
#
)时,Lua 会调用此元方法。它不能用于表格。 - __metatable
- 如果存在此元方法(可以是任何内容),则在表格上使用
getmetatable
将返回此元方法的值,而不是元表,并且不允许使用setmetatable
函数更改表格的元表。 - __mode
- 此元方法应该是一个字符串,其中可以包含字母 "k" 或 "v"(或两者)。如果存在字母 "k",则表格的键将是弱的。如果存在字母 "v",则表格的值将是弱的。这到底意味着什么将在后面解释,以及垃圾收集。
元表虽然正常的 Lua 程序只能将它们与表格一起使用,但实际上是 Lua 处理运算符和操作的核心机制,并且理论上它们实际上可以与任何值一起使用。但是,Lua 只允许将它们与表格和使用未记录的 newproxy
函数创建的用户数据值一起使用。使用 Lua 的 C API 或调试库,可以设置其他类型值(如数字和字符串)的元表。
有时希望在不调用元方法的情况下对表格执行操作。这对于索引、在表格中添加字段、检查相等性和使用 rawget
、rawset
、rawequal
和 rawlen
函数分别获取表格的长度是可能的。第一个返回对应于作为第二个参数给出的索引的值,该索引在作为第一个参数给出的表格中。第二个将作为第三个参数给出的值设置为对应于作为第二个参数给出的索引的值,该索引在作为第一个参数给出的表格中。第三个返回两个给定值是否相等。最后,第四个返回它给定对象的长度(一个整数),该对象必须是表格或字符串。
迭代器
[edit | edit source]迭代器是一个与 foreach 循环结合使用的函数。在大多数情况下,迭代器用于遍历数据结构。例如,由 pairs
和 ipairs
函数返回的迭代器分别用于遍历表格或数组的元素。例如,pairs
函数返回 next
函数,以及它作为参数给定的表格,这解释了 in pairs(dictionary)
与 in next, dictionary
的等效性,因为前者实际上评估为后者。
迭代器不需要始终与数据结构一起使用,因为迭代器可以针对需要循环的任何情况进行设计。例如,file:lines
函数返回一个迭代器,该迭代器在每次迭代时从文件中返回一行。类似地,string.gmatch
函数返回一个迭代器,该迭代器在每次迭代时返回字符串中模式的匹配项。例如,以下代码将打印名为“file.txt”的文件中的所有行。
for line in io.open("file.txt"):lines() do
print(line)
end
创建迭代器
[edit | edit source]迭代器包含三个部分
- 一个转换函数
- 一个状态值
- 一个或多个循环变量
转换函数用于修改每次循环迭代的循环变量(出现在 for
和 in
之间的变量)的值。此函数在每次迭代之前被调用,并以循环变量在上次迭代期间设置的值作为参数。该函数应返回一个包含这些变量的新值的元组(一个或多个值)。循环变量将设置为返回的元组的组件,并且循环将进行一次迭代。一旦该迭代完成(只要它没有被 break
或 return
语句中断),转换函数将再次被调用,并返回另一组值,循环变量将为下一次迭代设置这些值,依此类推。调用转换函数和迭代循环语句的这种循环将持续到转换函数返回 nil
为止。
除了循环变量之外,转换函数还会传入一个状态值,该状态值在整个循环中将保持不变。例如,状态值可用于维护对转换函数正在迭代的数据结构、文件句柄或资源的引用。
以下是一个将生成一系列数字(直到 10)的转换函数的示例。此转换函数仅需要一个循环变量,其值将存储在 value 中。
function seq(state, value)
if (value >= 10) then
return nil -- Returning nil will terminate the loop
else
local new_value = value + 1 -- The next value to use within the loop is the current value of `value` plus 1.
-- This value will be used as the value of `value` the next time this function is called.
return new_value
end
end
泛型 for 循环期望一个包含转换函数、状态值和循环变量初始值的元组。此元组可以直接包含在 in
关键字之后。
-- This will display the numbers from 1 to 10.
for value in seq, nil, 0 do
print(value)
end
但是,在大多数情况下,此元组将由一个函数返回。这允许使用迭代器工厂,它们在被调用时返回一个新的迭代器,该迭代器可以与泛型 for 循环一起使用。
function count_to_ten()
local function seq(state, value)
if (value >= 10) then
return nil
else
local new_value = value + 1
return new_value
end
end
return seq, nil, 0
end
for value in count_to_ten() do
print(value)
end
由于 Lua 支持闭包和函数作为一等公民,因此迭代器工厂也可以接受参数,这些参数可以在转换函数中使用。
function count_to(limit)
local function seq(state, value)
if (value >= limit) then
return nil
else
local new_value = value + 1
return new_value
end
end
return seq, nil, 0
end
for value in count_to(10) do -- Print the numbers from 1 to 10
print(value)
end
for value in count_to(20) do -- Print the numbers from 1 to 20
print(value)
end
标准库
[edit | edit source]Lua 是一种被称为“没有提供电池”的语言。这意味着它的库保持在执行某些任务所需的最低限度。Lua 依赖于其社区来创建可用于执行更特定任务的库。Lua 参考手册提供了所有库[1]的文档,因此这里将对其进行简要说明。除基本库和包库之外的所有库都将其函数和值作为表格的字段提供。
基本库
[edit | edit source]基本库为 Lua 提供核心功能。它的所有函数和值都直接在全局环境中可用,并且默认情况下,全局环境中直接可用的所有函数和值都是基本库的一部分。
断言
[edit | edit source]断言是一种谓词,开发人员假设它是正确的。它们在程序中用于确保在程序执行的特定时刻特定条件为真。断言用于单元测试中验证程序是否正常工作,但也用于程序代码中,在这种情况下,当断言为假时,程序将失败,要么为了验证程序所在的运行环境是否正确,要么为了验证程序代码中没有错误,并生成适当的错误消息,以便在某些事情没有按预期发生时更容易找到代码中的错误。在 Lua 中,断言是通过 assert
函数进行的,该函数接受一个条件和一个消息(默认值为“断言失败!”)作为参数。当条件评估为假时,assert
会抛出一个包含消息的错误。当它评估为真时,assert
将返回所有参数。
垃圾收集
[edit | edit source]垃圾收集是 Lua 和许多其他语言实现的一种自动内存管理形式。当程序需要将数据存储在变量中时,它会要求操作系统在计算机的内存中分配空间来存储变量的值。然后,当它不再需要空间时(通常是因为变量超出了作用域),它会告诉操作系统释放空间,以便其他程序可以使用它。在 Lua 中,实际过程要复杂得多,但基本思想是一样的:程序必须告诉操作系统它不再需要某个变量的值。在低级语言中,分配由语言处理,但释放则不是,因为语言无法知道程序员何时不再需要某个值:即使引用该值的变量超出了作用域或被删除,另一个变量或脚本中的字段可能仍然引用它,而释放它会导致问题。在高级语言中,释放可能由各种自动内存管理系统处理,例如垃圾收集,它是 Lua 使用的系统。垃圾收集器会定期搜索 Lua 分配的所有值,以查找任何地方都没有引用的值。它将收集程序无法访问的任何值(因为没有引用它们),并且由于它知道这些值可以安全地释放,因此它将释放它们。所有这些都以透明且自动的方式完成,因此程序员通常不需要为此做任何事情。但是,有时开发人员可能希望向垃圾收集器发出指令。
弱引用
[edit | edit source]弱引用是指垃圾回收器会忽略的引用。这些引用由开发人员使用 `mode` 元方法指示给垃圾回收器。表的 `mode` 元方法应该是一个字符串。如果该字符串包含字母 "k",则该表的所有字段键都是弱引用,如果包含字母 "v",则该表的所有字段值都是弱引用。当对象数组具有弱引用值时,即使这些对象在该数组中被引用,只要它们只在该数组和其它弱引用中被引用,垃圾回收器就会收集这些对象。这种行为有时很有用,但很少使用。
可以使用 `collectgarbage` 函数操作垃圾回收器,该函数是基本库的一部分,用作垃圾回收器的接口。它的第一个参数是一个字符串,指示垃圾回收器应该执行什么操作;第二个参数由某些操作使用。`collectgarbage` 函数可用于停止垃圾回收器,手动执行收集周期并统计 Lua 使用的内存。
—维基百科, 协程
协程是可以通过 Lua 中的协程库创建和操作的组件,允许通过调用从自身内部产生协程的函数或从自身外部恢复协程的函数,在特定位置产生和恢复函数的执行。例如
- 主线程中的一个函数使用 `coroutine.create` 从一个函数创建一个协程,并使用 `coroutine.resume` 恢复它,并将数字 3 传递给它。
- 协程中的函数执行并获取传递给 `coroutine.resume` 作为参数的数字。
- 函数在其执行过程中的某个点调用 `coroutine.yield`,并将它接收到的参数(3)与 2 的总和(因此为 3+2=5)作为参数传递。
- 对 `coroutine.resume` 的调用返回 5,因为它被传递给了 `coroutine.yield`,并且现在再次运行的主线程将该数字存储在一个变量中。在执行了一些代码之后,它再次恢复协程,并将 `coroutine.resume` 的调用结果(即 5×2=10)的双倍传递给 `coroutine.resume`。
- 协程将传递给 `coroutine.resume` 的值作为对 `coroutine.yield` 的调用的结果,并在运行更多代码后终止。它返回对 `coroutine.yield` 的调用的结果与其最初作为参数传递给它的值之间的差值(即 10−3=7)。
- 主线程获取协程作为对 `coroutine.resume` 的调用的结果返回的值,然后继续运行。
这个示例用代码表示如下
local co = coroutine.create(function(initial_value)
local value_obtained = coroutine.yield(initial_value + 2) -- 3+2=5
return value_obtained - initial_value -- 10-3=7
end)
local _, initial_result = coroutine.resume(co, 3) -- initial_result: 5
local _, final_result = coroutine.resume(co, initial_result * 2) -- 5*2=10
print(final_result) --> 7
`coroutine.create` 函数从一个函数创建一个协程;协程是类型为 "thread" 的值。`coroutine.resume` 启动或继续执行协程。当协程遇到错误或没有剩余要运行的内容(在这种情况下它已经终止了执行)时,协程被称为死亡。死亡的协程无法恢复。如果协程的执行成功,`coroutine.resume` 函数将返回 `true` 以及所有返回值(如果协程已终止)或传递给 `coroutine.yield` 的值(如果尚未终止)。如果执行不成功,它将返回 `false` 以及错误消息。`coroutine.resume` 返回运行的协程和 `true`(当该协程为主线程时),否则返回 `false`。
`coroutine.status` 函数以字符串形式返回协程的状态
- "running" 如果协程正在运行,这意味着它必须是调用 `coroutine.status` 的协程
- "suspended" 如果协程挂起在对 `yield` 的调用中,或者它尚未开始运行
- "normal" 如果协程处于活动状态但未运行,这意味着它已恢复另一个协程
- "dead" 如果协程已完成运行或遇到错误
`coroutine.wrap` 函数返回一个函数,该函数在每次调用时都会恢复协程。传递给该函数的额外参数将作为 `coroutine.resume` 的额外参数,协程返回的值或传递给 `coroutine.yield` 的值将由对该函数的调用返回。`coroutine.wrap` 函数与 `coroutine.resume` 不同,它不会在保护模式下调用协程,而是会传播错误。
协程有很多用例,但描述它们超出了本书的范围。
在操作字符串时,能够在字符串中搜索符合特定模式的子字符串通常很有用。Lua 具有一个字符串操作库,该库提供了用于执行此操作的函数和用于表达这些函数可以在字符串中搜索的模式的符号。Lua 提供的符号与正则表达式非常相似,正则表达式是大多数编程语言和工具使用的用于表达模式的符号。但是,它更加有限,语法略有不同。
字符串库的 `find` 函数在字符串中查找模式的第一个匹配项。如果它在字符串中找到了模式的出现,它将返回字符串中模式出现的位置的索引(整数,表示字符串中字符的位置,从第一个字符开始,位置为 1)。如果它没有找到模式的出现,它将不返回任何内容。它接受的第一个参数是字符串,第二个参数是模式,第三个参数是一个整数,表示 `find` 函数应该开始搜索的字符位置。最后,可以通过向 `find` 函数提供 `true` 作为其第四个参数来告诉它执行简单的匹配而不需要模式。然后它将简单地搜索在第一个字符串中找到的第二个字符串的出现。当执行简单匹配时,必须给出第三个参数。此示例代码在句子中搜索 "lazy" 一词,并打印它找到的该词的出现位置的开始和结束位置
local start_position, end_position = string.find("The quick brown fox jumps over the lazy dog.", "lazy", 1, true)
print("The word \"lazy\" was found starting at position " .. start_position .. " and ending at position " .. end_position .. ".")
此代码的结果为 The word "lazy" was found starting at position 36 and ending at position 39.。它等同于以下代码
local sentence = "The quick brown fox jumps over the lazy dog."
local start_position, end_position = sentence:find("lazy", 1, true)
print("The word \"lazy\" was found starting at position " .. start_position .. " and ending at position " .. end_position .. ".")
这是有效的,因为字符串的 `index` 元方法被设置为包含字符串库函数的表,从而可以将 `string.a(b, ...) ` 替换为 `b:a(...)`。
字符串库中接受索引来指示字符位置或返回此类索引的函数将第一个字符视为位置 1。它们接受负数并将它们解释为从字符串末尾开始反向索引,最后一个字符位于位置 -1。
模式是遵循特定符号的字符串,用于指示字符串是否可以匹配的模式。为此,模式包含字符类,这些字符类代表字符集。
字符组合 | 描述 |
---|---|
. | 所有字符 |
%a | 字母(大写和小写) |
%c | 控制字符 |
%d | 数字 |
%g | 可打印字符(空格字符除外) |
%l | 小写字母 |
%p | 标点符号 |
%s | 空格字符 |
%u | 大写字母 |
%w | 字母数字字符(数字和字母) |
%x | 十六进制数字 |
所有不是特殊字符的字符都代表自身,特殊字符(所有不是字母数字的字符)可以通过在前面加上百分号来转义。可以通过将字符类放在集合中来组合字符类以创建更大的字符类。集合表示为方括号之间表示的字符类(例如 `[%xp]` 是所有十六进制字符加上字母 "p" 的集合)。字符范围可以通过用连字符(例如 `[0-9%s]` 表示从 0 到 9 的所有数字加空格字符)分隔范围的结束字符(按升序)来表示。如果在集合的开头(紧接在开方括号之后)放置插入符号 ("^") 字符,则该集合将包含所有字符,除了它在插入符号未放置在集合的开头时会包含的字符。
可以通过百分号后跟相应的字母大写来表示由百分号前缀的字母表示的所有类的补集(例如 `%S` 表示所有非空格字符)。
模式是代表应在模式中找到哪些序列才能使字符串与之匹配的模式项序列。模式项可以是一个字符类,在这种情况下它将匹配该类中的任何字符一次,一个字符类后跟 "*" 字符,在这种情况下它将匹配该类中字符的 0 次或多次重复(这些重复项将始终匹配最长的可能序列),一个字符类后跟加号 ("+") 字符,在这种情况下它将匹配该类中字符的 1 次或多次重复(这些重复项也将始终匹配最长的可能序列),一个字符类后跟减号 ("-") 字符,在这种情况下它将匹配该类中字符的 0 次或多次重复,但将匹配最短的可能序列,或者一个字符类后跟一个问号,在这种情况下它将匹配该类中字符的一次或零次出现。
可以匹配与之前捕获的子字符串等效的子字符串:%1
将匹配第一个捕获的子字符串,%2
匹配第二个,依此类推,直到 %9
。捕获在下面描述。模式还提供其他两种功能,如参考手册所述。
%bxy
,其中 x 和 y 是两个不同的字符;此项匹配以 x 开头、以 y 结尾的字符串,并且 x 和 y 是平衡的。这意味着,如果从左到右读取字符串,对 x 计数 +1,对 y 计数 -1,则结束符 y 是计数达到 0 的第一个 y。例如,项%b()
匹配具有平衡括号的表达式。
%f[set]
,一个边界模式;此项在任何位置匹配空字符串,使得下一个字符属于集合而前一个字符不属于集合。集合 set 的解释如前所述。主题的开头和结尾被视为字符 '\0'。——Lua 作者, Lua 5.2 参考手册
模式是模式项的序列,可选地以插入符号开头,表示模式只能在字符串开头匹配,可选地以美元符号结尾,表示模式只能在字符串结尾匹配。这些符号被称为锚定在字符串的开头或结尾匹配。这两个字符只有在模式的开头或结尾时才具有特殊含义。
子模式可以包含在模式中的括号内,以指示捕获。当匹配成功时,与捕获匹配的字符串的子字符串将被存储以供将来使用,例如由 gmatch
返回。它们总是从其左括号的位置开始编号。两个空括号表示空捕获,它捕获当前字符串位置(这是一个数字,不是字符串的一部分)。
gmatch
函数可用于遍历字符串中模式的出现次数;与 find
函数不同,它不能指定要开始搜索的初始位置或执行简单匹配。gmatch
函数返回一个迭代器,当调用时,它将从字符串中的给定模式返回下一个捕获。如果模式中没有指定捕获,则将给出整个匹配项。以下示例展示了如何遍历句子中的单词并将它们逐个打印出来
local sentence = "The quick brown fox jumps over the lazy dog."
for word in sentence:gmatch('%a+') do
print(word)
end
在这个示例中,整个匹配项由迭代器返回的唯一值 word 给出。
gsub
函数可用于将字符串中模式的所有出现次数替换为其他内容。其前两个参数是字符串和模式,第三个参数是要替换出现的字符串,第四个参数是要替换的出现次数的最大值。第三个参数可以是表或函数,而不是字符串。
当第三个参数是字符串时,它被称为替换字符串,它将替换字符串中模式的出现次数。模式存储的捕获可以嵌入到替换字符串中;它们由百分号后跟表示捕获编号的数字来表示。匹配本身可以用 %0
表示。替换字符串中的百分号必须转义为 %%
。
当第三个参数是表时,第一个捕获将用作键来索引该表,替换字符串是该表中对应于该键的值。当它是函数时,该函数将为每个匹配项调用,并将所有捕获作为参数传递。在这两种情况下,如果不存在捕获,则使用整个匹配项。如果函数或表给出值 false
或 nil
,则不会进行替换。
以下是一些直接摘自 Lua 5.2 参考手册的示例
x = string.gsub("hello world", "(%w+)", "%1 %1") --> x="hello hello world world" x = string.gsub("hello world", "%w+", "%0 %0", 1) --> x="hello hello world" x = string.gsub("hello world from Lua", "(%w+)%s*(%w+)", "%2 %1") --> x="world hello Lua from" x = string.gsub("home = $HOME, user = $USER", "%$(%w+)", os.getenv) --> x="home = /home/roberto, user = roberto" x = string.gsub("4+5 = $return 4+5$", "%$(.-)%$", function (s) return load(s)() end) --> x="4+5 = 9" local t = {name="lua", version="5.2"} x = string.gsub("$name-$version.tar.gz", "%$(%w+)", t) --> x="lua-5.2.tar.gz"——Lua 作者, Lua 5.2 参考手册
Lua 提供了其他用于操作字符串的函数,而不是用于模式匹配的函数。这些包括 reverse
函数,它返回一个字符顺序颠倒的字符串,lower
函数,它返回字符串的小写等效项,upper
函数,它返回字符串的大写等效项,len
函数,它返回字符串的长度,以及 sub
函数,它返回字符串的子字符串,该子字符串从给定为参数的两个字符位置开始并结束。还有更多,它们的文档可以在参考手册中找到。
- ↑ Ierusalimschy, Roberto; Celes, Waldemar; Henrique de Figueiredo, Luiz. Lua 5.2 参考手册. Retrieved 30 November 2013.
术语表
[edit | edit source]这是一个包含与 Lua 中的编程相关的术语的术语表。建议使用它来查找不理解的单词的含义。
- 抽象类
- 一个抽象类是一个不能直接创建实例的类。抽象类是抽象类型。
- 抽象数据类型
- 一个抽象数据类型是一个模型,用于表示具有类似行为的一类数据结构。抽象数据类型由可以在其上执行的操作以及这些操作的数学约束来定义,而不是由实现和数据在计算机内存中的存储方式来定义。
- 抽象类型
- 一个抽象类型是一种数据类型,不能直接创建其实例。
- 实际参数
- 见参数。
- 加法逆元
- 一个数字的加法逆元是当加到该数字上时会产生零的数字。例如,42 的加法逆元是 -42。
- 算术否定
- 算术否定是产生一个数字的加法逆元的操作。
- 算术运算
- 一个算术运算是一个操作,其操作数是数字。
- 元数
- 一个操作或函数的元数是操作或函数接受的操作数或参数的个数。
- 参数
- 一个参数是传递给函数的值。
- 数组
- 一个数组是一个数据结构,它由一组值组成,每个值至少由一个数组索引或键标识。
- 关联数组
- 一个关联数组是一个抽象数据类型,它由一组键值对组成,使得每个可能的键在集合中最多出现一次。
- 增强赋值
- 增强赋值是一种赋值类型,它给变量赋予一个相对于其先前值的价值。
- 二元运算
- 一个二元运算是一个元数为 2 的运算。
- 布尔值
- 见逻辑数据。
- 布尔否定
- 见逻辑否定。
- 链式赋值
- 链式赋值是一种赋值类型,它将单个值赋予多个变量。例如:
a = b = c = d = 0
。 - 代码块
- 代码块是一系列语句。
- 复合赋值
- 见增强赋值。
- 串联
- 字符串串联是连接两个字符字符串的操作。例如,"snow" 和 "ball" 的串联是 "snowball"。
- 具体类
- 一个具体类是一个可以创建其实例的类。具体类是具体类型。
- 具体类型
- 一个具体类型是一种可以创建其实例的类型。
- 条件
- 一个条件是一个谓词,它用在条件语句中或作为条件运算符的操作数。在 Lua 中,条件在它们的表达式计算出除
nil
或false
之外的值时被视为真,否则被视为假。 - 条件运算符
- 一个条件运算符是一个运算符,它在条件为真时返回一个值,而在条件为假时返回另一个值。
- 条件语句
- 一个条件语句是一个语句,它在条件为真时执行一段代码。
- 数据结构
- 一个数据结构是在计算机内存中存储和组织数据的特定方式。它是抽象数据类型的实现。
- 数据类型
- 数据类型是用来描述计算机内存中数据存储方式的模型。
- 字典
- 参见 关联数组。
- 异或
- 异或运算是一种二元运算,当其中一个操作数为真而另一个操作数为假时,它产生值为真。 a和 b的异或在数学上表示为 。 Lua中没有与异或对应的运算符,但 可以用
(a or b) and not (a and b)
表示。 - 形式参数
- 参见 参数。
- 函数
- 函数是一系列执行特定任务的语句(指令)。函数可以在程序中需要执行该特定任务的任何地方使用。函数通常在将要使用它们的程序中定义,但也有些函数是在库中定义的,其他程序可以使用这些库。
- 哈希表
- 参见 哈希表。
- 哈希表
- 哈希表是作为 数据结构 实现的 关联数组。哈希表使用 哈希函数 来计算一个数组(桶或槽)中的索引,然后根据这个索引找到对应的值。
- 内联 if
- 参见 条件运算符。
- 整数
- 整数是可以不带分数或小数部分而写出的数字。在 Lua 中,整数的实现方式与其他数字相同。
- 长度运算
- 长度运算是一种运算,用来计算数组中值的个数。
- 字面量
- 字面量是在源代码中表示固定值的一种表示法。除线程和用户数据外,所有值都可以在 Lua 中用字面量表示。
- 逻辑非
- 布尔值的逻辑非是与该值不同的布尔值。这意味着 `true` 的逻辑非是 `false`,反之亦然。
- 逻辑与
- 逻辑与运算是一种二元运算,当两个操作数都为真时,它产生值为真,在其他所有情况下,它产生值为假。 它在 Lua 中实现为 `and` 运算符,如果第一个操作数为 `false` 或 `nil`,则返回第一个操作数,否则返回第二个操作数。 a 和 b 的逻辑与在数学上表示为 。
- 逻辑数据
- 逻辑数据类型,通常称为布尔类型,是 `false` 和 `true` 这些值的数据类型。
- 逻辑或
- 逻辑或运算是一种二元运算,当两个操作数都为假时,它产生值为假,在其他所有情况下,它产生值为真。 它在 Lua 中实现为 `or` 运算符,如果第一个操作数既不是 `false` 也不是 `nil`,则返回第一个操作数,否则返回第二个操作数。 a 和 b 的逻辑或在数学上表示为 。
- 逻辑否
- 逻辑否,在 Lua 中由 `not` 运算符实现,是产生布尔值 逻辑非 的运算。
- 映射
- 参见 关联数组。
- 方法
- 方法是对象的一个成员函数,通常对该对象进行操作。
- 模
- 参见 模运算。
- 模运算
- 模运算,在 Lua 中由 `%` 运算符实现,是产生一个数字被另一个数字除后的余数的运算。
- 模数
- 参见 模运算。
- 多重赋值
- 参见 并行赋值。
- 空值
- 空值类型是 `nil` 值的类型,其主要特点是与任何其他值都不同;它通常表示没有有用的值。
- 非运算符
- 见逻辑否定。
- 数字
- 数字类型表示实数 (双精度浮点数)。 Lua 解释器可以被构建为使用其他内部表示来表示数字,例如单精度浮点数或长整数。
- 运算符
- 运算符是一个标记,它从一个或多个操作数生成一个值。
- 并行赋值
- 并行赋值是一种同时将值赋给不同变量的赋值类型。
- 参数
- 参数是函数定义中的一个变量,函数调用中与它对应的参数将被赋给它。
- 谓词
- 谓词是一个表达式,它计算出一个逻辑数据。
- 过程
- 参见 函数。
- 关系运算符
- 关系运算符是用来比较值的运算符。
- 例程
- 参见 函数。
- 符号变更
- 参见 算术否定。
- 同时赋值
- 参见 并行赋值。
- 字符串
- 字符串类型表示字符数组。 Lua 是 8 位安全的:字符串可以包含任何 8 位字符,包括嵌入的零。
- 字符串字面量
- 字符串字面量是计算机程序源代码中表示字符串值的表示法。就语法而言,字符串字面量是一个表达式,它计算出一个字符串。
- 子程序
- 参见 函数。
- 子例程
- 参见 函数。
- 符号
- 参见 标记。
- 符号表
- 符号表是作为数据结构实现的关联数组。 它们通常用哈希表实现。
- 标记
- 标记是数据的原子片段,例如人类语言中的词语或编程语言中的关键字,在解析过程中可以推断其含义。
- 变量
- 一个变量是与内存中某个位置关联的标签。 该位置的数据可以更改,变量将指向新数据。
- 可变参数函数
- 一个可变参数函数是具有不定元数的函数。
这是一个按字母顺序排列的书籍索引。
- 作用域
- self参数
- 语义错误
- Shake
- 短注释
- 同时赋值
- 软件测试
- 排序表
- 堆栈
- 堆栈溢出
- 语句
- 状态值
- 静态测试
- 字符串
- 字符串连接
- 字符串操作
- 字符串匹配
- 字符串模式
- 符号表
- 语法错误
- 语法
- 系统测试
Lua API 中的函数和变量有一个单独的索引。该索引指向书中提到 API 中函数或变量的部分。
- assert
- collectgarbage
- dofile
- error
- getmetatable
- ipairs
- load
- loadfile
- next
- pairs
- pcall
- rawequal
- rawget
- rawlen
- rawset
- select
- setmetatable
- tonumber
- tostring
- type
- xpcall
- string.dump
- string.find
- string.gmatch
- string.gsub
- string.len
- string.lower
- string.reverse
- string.sub
- string.upper