Lua-程序设计-Notes
1 Lua 语言入门
1.1 程序段
程序段 (Chunk), 一组命令或表达式组成的序列.
-i
参数, 在执行完指定的程序段后进入交互模式:
1 |
|
调用函数 dofile
可以加载一个文件:
1 |
|
就加载了 lib1.lua
文件.
1.2 一些词法规范
“下划线+大写字母” 组成的标识符通常被 Lua 语言用作特殊用途.
Lua 对大小写敏感.
--
为单行注释.
长注释为:
1 |
|
Lua 中连续语句之间的分隔符并不是必需的, 如果有需要的话可以使用分号来进行分隔, 以下等同:
1 |
|
1.3 全局变量
全局变量无需声明即可使用.
使用未初始化的全局变量不会报错, 得到 nil
.
把 nil
赋值给全局变量, Lua 会回收该全局变量.
Lua 不区分未初始化变量和被赋值为 nil
的变量.
1.4 类型和值
基本类型有:
- nil
- boolean
- number
- string
- userdata
- function
- thread
- table
使用 type
函数查看:
1 |
|
userdata
类型允许把任意的 C 语言数据保存在 Lua 语言变量中.
1.4.1 nil
nil
表示无效值.
1.4.2 Boolean
具有两个值:
- true
- false
可用逻辑运算符:
and
, 如果它的第一个操作数为false
, 则返回第一个操作数, 否则返回第二个操作数or
, 如果它的第一个操作数不为false
, 则返回第一个操作数, 否则返回第二个操作数not
, 永远返回 Boolean 类型的值
1.5 独立解释器
在脚本开头添加:
1 |
|
不需要显式调用 Lua 语言解释器也可以直接运行 Lua 脚本.
-e
参数允许在命令行输入代码:
1 |
|
-l
参数用来加载库.
解释器在处理参数前, 会查找名为 LUA_INIT_5_3
或 LUA_INIT
的环境变量
- 如果变量内容为
@filename
则会运行相应文件 - 如果变量存在但是不以
@
开头, 解释器就会认为其包含 Lua 代码, 并会对其进行解释执行
编译器在运行代码前会创建一个名为 arg
的表, 其中存储了所有的命令行参数, 索引 0
保存脚本名, 索引 1
保存第一个参数:
1 |
|
2 八皇后问题 (eight-queen puzzle)
其目标是把 8 个皇后合理地摆放在棋盘上, 让每个皇后之间都不能相互攻击.
3 数值
从 Lua 5.3 版本开始, Lua 语言为数值格式提供了两种选择:
- 被称为
integer
的 64 位整型 - 被称为
float
的双精度浮点类型
3.1 数值常量
具有十进制小数或者指数的数值会被当作浮点型值.
整型值和浮点型值的类型都是 number
.
当需要区分整型值和浮点型值时, 使用函数 math.type
.
//
floor 除法运算符, 会将得到的商向负无穷取整, 从而保证结果是一个整数. 这样可以遵循一个规则: 如果操作数都是整型值, 那么结果就是整型值, 否则就是浮点型值.
3.3 关系运算
<
>
<=
>=
==
~=
, 不等测试
3.4 数学库
标准数学库 math
.
所有三角函数都以弧度为单位, 并通过函数 deg
和 rad
进行角度和弧度的转换.
math.random
用于生成伪随机数.
randomseed
用于设置伪随机数发生器的种子, 通常调用 math.randomseed(os.time())
3.4.2 取整函数
三个取整函数:
floor
, 向负无穷取整ceil
, 向正无穷取整modf
, 向零取整, 除了返回取整后的值以外, 还会返回小数部分作为第二个结果
3.5 表示范围
最小值和最大值:
math.mininteger
math.maxinteger
3.6 惯例
通过增加 0.0
的方法将整型值强制转换为浮点型值:
1 |
|
3.7 运算符优先级
4 字符串
Lua 语言中的字符串是一串字节组成的序列.
字符使用 8 个比特位来存储.
Lua 语言的字符串是不可变值 (immutable value), 不能像 C 语言中那样直接修改某个字符串中的某个字符. 但可以通过创建一个新字符串的方式来达到修改的目的:
1 |
|
获取字符串长度, 使用 #
长度操作符:
1 |
|
返回字符串占用的字节数.
连接操作符 ..
4.1 字符串常量
双引号和单引号是等价的.
支持 C 语言风格的转义字符:
4.2 长字符串/多行字符串
使用一对双方括号来声明长字符串/多行字符串常量:
1 |
|
字符串中遇到 a=b[c[i]]
的处理方法, 左方括号变为 [===[
, 即在两个左方括号之间加上任意数量的等号, 这样只有遇到了包含相同数量等号的两个右方括号时才会结束. 该方法对注释同样有效.
4.3 强制类型转换
最好不依赖自动转换.
使用 tonumber()
, tostring()
等函数.
4.4 字符串标准库
如:
string.len()
string.rep()
, 重复string.reverse()
string.lower()
string.upper()
string.sub()
, 不会改变原有字符串的值, 指挥返回一个新字符串string.char()
string.byte()
string.format()
string.find("hello world", "wor")
, 返回模式的开始和结束位置string.gsub()
如:
1 |
|
可用冒号操作符 :
直接调用:
1 |
|
4.5 Unicode 编码
5 表 (table)
表 (table) 是 Lua 语言中最主要 (事实上也是唯一的) 数据结构.
调用函数 math.sin
对于 Lua 语言来说, 其实际含义是: 以字符串 “sin” 为键检索表 math.
5.1 表索引
同一个表中存储的值可以具有不同的类型索引, 并可以按需增长以容纳新的元素:
1 |
|
可以把表当结构体使用:
1 |
|
5.2 表构造器
Table Constructor, 用来创建和初始化表的表达式.
空构造器: {}
初始化列表式:
1 |
|
初始化记录式 (record-like) 表:
1 |
|
无论使用哪种方式创建表, 都可以随时增加或删除表元素.
1 |
|
混用:
1 |
|
更加通用的构造器, 通过方括号括起来的表达式显式指定每一个索引:
1 |
|
5.3 数组, 列表和序列
使用整型作为索引的表, 不需要预先声明表的大小:
1 |
|
Lua 中, 数组的索引从 1 开始.
通常, 把列表的长度保存在表中某个非数值类型的字段中, 由于历史原因, 这个键通常是 “n”.
可以用 nil
值来标记列表的结束.
#
操作符也可以获取列表长度.
对于 Lua 语言而言, 一个为 nil
的字段和一个不存在的元素没有区别:
1 |
|
长度为 3 而不是 5.
处理存在空洞的列表时, 应该将列表的长度显式地保存起来.
5.4 遍历表
pairs
迭代器, 可得到表中的键值对:
1 |
|
受限于表在 Lua 语言中的底层实现机制, 遍历过程中元素的出现顺序可能是随机的.
对于列表 (上面的不算是列表) 可使用 ipairs
迭代器, 可以保证按照顺序.
for
循环遍历:
1 |
|
5.5 安全访问
Lua 没有安全访问操作符.
a or {}
当 a
为 nil
时其结果是一个空表, 对于表达式 (a or {}).b
, 当 a
为 nil
时其结果也同样是 nil
.
5.6 表标准库
table.insert
向序列指定位置插入一个元素, 不指定位置时在序列的最后插入:
1 |
|
另一个例子:
1 |
|
table.remove
删除并返回序列指定位置的元素, 然后将其后的元素向前豫东填充删除元素后造成的空洞.
不指定位置则删除最后一个.
1 |
|
table.move
1 |
|
将表 a
中从索引 f 到 e 的元素移动到位置 t 上:
1 |
|
table.move
还支持使用一个表作为可选参数, 即将第一个表中的元素移动到第二个表中.
6 函数
无论哪种情况, 函数调用时都需要使用一对圆括号把参数列表括起来.
例外 当函数只有一个参数且该参数是字符串常量或表构造器:
1 |
|
Lua 为面向对象提供的特殊语法, 冒号操作符 :
如:
1 |
|
Lua 语言标准库中所有的函数都是使用 C 语言编写的.
函数语法格式 :
1 |
|
参数的行为与局部变量的行为完全一致, 相当于一个用函数调用时传入的值进行初始化的局部变量.
调用函数时使用的参数个数可以与定义函数时使用的参数不一致.
Lua 语言会通过抛弃多余参数和将不足的参数设为 nil
的方式来调整参数的个数.
默认参数:
1 |
|
这里 n
的默认值相当于就是 1.
6.1 多返回值
Lua 允许一个函数返回多个结果.
在 return
后列出所有要返回的值即可:
如:
1 |
|
在多重赋值中, 如果一个函数没有返回值或者返回值个数不够多, 那么 Lua 语言会用 nil
来补充缺失的值:
1 |
|
注意, 只有当函数调用是一系列表达式中的最后一个表达式时才能返回多值结果, 否则只能返回一个结果.
1 |
|
当一个函数调用是另一个函数调用的最后一个实参时, 第一个函数的所有返回值都会被作为实参传给第二个函数.
将函数调用用用一对圆括号括起来可以强制其只返回一个结果:
1 |
|
6.2 可变长参数函数
如:
1 |
|
...
表示该函数的参数是可变长的.
当函数要访问这些参数时仍需用到 ...
表达式 {...}
(被称为可变长参数表达式 vararg expression) 的结果是一个由所有可变长参数组成的列表.
可用变长参数来模拟 Lua 中普通的参数传递机制:
1 |
|
格式化输出的函数 string.format
. 输出文本函数 io.write
具有可变长参数的函数也可以具有任意数量的固定参数:
1 |
|
table.pack
函数, 保存所有参数, 然后将其放在一个表中返回, 这个表中还有一个保存了参数个数的额外字段 “n”.
判断可变长参数中是否包含无效的 nil
:
1 |
|
使用 select 函数遍历
函数 select
总是具有一个固定的参数 selector
, 以及数量可变的参数, 如果 selector
是数值 n, 那么函数 select 则返回第 n 个参数后的所有参数, 否则, selector
应该是字符串 #
, 以便函数 select
返回额外参数的总数:
1 |
|
遍历如:
1 |
|
6.3 函数 table.unpack
其参数是一个数组, 返回值为数组内的所有元素.
1 |
|
通过数组 a 传入可变的参数来调用函数 f:
1 |
|
两个等价形式:
1 |
|
等价于:
1 |
|
可以显示地限制返回元素的范围
1 |
|
6.4 正确的尾调用
当一个函数的最后一个动作是调用另一个函数而没有进行其他工作时, 就形成了 尾调用 (tail call)
如:
1 |
|
当 g 返回时, 程序的执行路径会直接返回到调用 f 的位置. 不会再调用栈中保存有关调用函数的任何信息.
在进行尾调用时不使用任何额外的栈空间,称这种实现为尾调用消除 (tail-call elimination)
由于尾调用不会使用栈空间, 所以一个程序中能够嵌套的尾调用的数量是无限的. (不会发生栈溢出)
7 输入输出
Lua 只提供了 ISO C 语言标准支持的功能, 即基本的文件操作等.
7.1 简单 I/O 模型
I/O 操作通过:
- 当前输入流 (current input stream), 初始化为标准输入 stdin
- 当前输出流 (current output stream), 初始化为标准输出 stdout
实现.
函数 io.input 和 io.output
函数 io.input
和函数 io.output
可以用于改变当前的输入输出流.
如: io.input(filename)
会以只读模式打开指定文件, 并将文件设置为当前输入流.
函数 io.write 和 io.read
io.write
读取任意数量的字符串 (或者数字) 并将其写入当前输出流.
作为原则, 应该只在 “用后即弃” 的代码或调试代码中使用函数 print
, 当需要完全控制输出时, 应该使用函数 io.write
.
函数 io.write
允许对输出进行重定向. 函数 print
只能使用标准输出, 其可以自动为其参数调用 tostring
.
io.read
可以从当前输入流中读取字符串, 其参数有:
如:
1 |
|
使用 io.lines
迭代器, 逐行迭代一个文件:
1 |
|
io.read(0)
是一个特例, 它常用于测试是否到达了文件末尾, 如果仍然有数据可供读取, 它会返回一个空字符串, 否则, 则返回 nil.
调用 read
时可以指定多个选项, 函数会根据每个参数返回相应的结果, 如:
1 |
|
7.2 完整 I/O 模型
io.open
两个参数:
- 待打开的文件名
- 模式, 包括: 只读的
r
, 只写的w
, 追加的a
, 打开二进制文件的b
检查错误, 使用 assert
函数:
1 |
|
错误信息会作为函数 assert
的第二个参数被传入, 之后函数 assert
会将错误信息展示出来.
在打开文件后, 可以使用方法 read
和 write
从流中读取和向流中写入, 需要 使用冒号运算符 将它们当作流对象的方法来调用:
1 |
|
三个预定义的 C 语言流的句柄:
io.stdin
io.stdout
io.stderr
混用 I/O 模型:
1 |
|
注意:
io.read(args)
实际上是io.input():read(args)
的简写io.write(args)
实际上是io.input():write(args)
的简写
7.3 其他文件操作
io.tmpfile
返回一个操作临时文件的句柄, 其以读/写模式打开.
flush
setvbuf
seek
whence
os.rename
os.remove
7.4 其他系统调用
os.exit
os.getenv
7.4.1 运行系统命令
os.execute
等价于 C 语言中的函数 system.
io.pipen
8 补充知识
8.1 局部变量和代码块
Lua 语言中的变量在默认情况下是全局变量, 所有的局部变量在使用前必须声明.
局部变量的生效范围仅限于声明它的代码块.
在 交互模式 中, 每一行代码就是一个代码段. 可以用 do-end
语句显式声明整个代码块:
1 |
|
尽可能使用局部变量是一种良好的编程风格.
Lua 语言的发行版中有一个用于全局变量检查的模块 strict.lua
, 如果试图在一个函数中对不存在的全局变量赋值或者使用不存在的全局变量, 将会抛出异常.
8.2 控制结构
所有的控制结构语法上都有一个显式的终结符: end
注意, Lua 语言将所有不是 false
和 nil
的值当作真.
8.2.1 if then else
1 |
|
then
表达式可以作为条件表达式的终止标记.
error
函数用于报错.
Lua 不支持 switch
语句.
8.2.2 while
1 |
|
8.2.3 repeat
1 |
|
8.2.4 数值型 for
两种类型的 for
语句:
- 数值型 (numerical)
- 泛型 (generic)
数值型如:
1 |
|
var
的值从 exp1
变化到 exp2
, exp3
是步长 (step) 可选, 默认为 1.
不设上限, 使用 math.huge
:
1 |
|
这里的控制变量 i
是局部变量.
注意, 不要改变控制变量的值, 随意改变控制变量的值可能产生不可预知的结果.
8.2.5 泛型 for
泛型 for
遍历迭代函数返回的所有值. (如 pairs
, ipairs
, io.lines
)
泛型 for
可以使用多个变量, 这些变量在每次循环时都会更新, 当第一个变量变为 nil
时 循环终止.
1 |
|
8.3 break, return 和 goto
所有函数的最后都有一个隐含的 return
.
goto
的标签形式如 ::name::
. (复杂语法用于醒目标识)
goto
不能跳转到函数外, 也不能跳转到局部变量的作用域.
局部变量的作用域终止于声明变量的代码块中的最后一个有效 (non-void) 语句处, 标签被认为是无效 (void) 语句.
9 闭包
一个概念, Lua 的函数是遵循 词法定界 (lexicalscoping) 的第一类值 (first-classvalue)
第一类值 指 Lua 中的函数与其他常见的类型的值具有同等权限:
- 可以将函数保存在变量或表中
- 可以将函数作为参数传递给其他函数
- 可以将函数作为其他函数的返回值
词法定界 指 Lua 中的函数可以访问包含其自身的外部函数中的变量. 如函数 B 包含函数 A, 那么函数 A 可以访问函数 B 的所有局部变量.
9.1 第一类值
1 |
|
函数的定义可以由:
1 |
|
写成:
1 |
|
注意, 在 Lua 中所有函数都是匿名的 (anonynous), 即没有名字, 当讨论函数名如 print
时, 实际上指的是保存该函数的变量.
高阶函数, 以另一个函数作为参数, 如 sort
:
1 |
|
9.2 非全局函数
表字段中存储函数, 有很多中写法:
1 |
|
或:
1 |
|
或:
1 |
|
当把一个函数存储到局部变量时, 就得到了一个局部函数 (local function).
定义局部函数:
1 |
|
其展开为:
1 |
|
9.3 词法定界
闭包 (closure). 即一个持有外部环境变量的函数就是闭包.
由于函数可以被保存在普通变量中, 因此 Lua 语言中可以轻松地重定义函数:
1 |
|
10 模式匹配
10.1 模式匹配相关函数
字符串标准库提供的 4 个函数:
find
gsub
(global substitute)match
gmatch
(global match)
10.1.1 string.find
两个参数:
- 字符串
- 要匹配的值
两个可选参数:
- 第三个, 整数, 从那里开始的索引
- 第四个, 布尔值, 是否进行简单搜索
匹配成功返回两个值:
- 模式开始位置的索引
- 模式结束位置的索引
没找到任何匹配, 返回 nil
.
1 |
|
10.1.2 string.match
和 string.find
类似, 但是返回字符串.
1 |
|
10.1.3 string.gsub
3 个必选参数:
- 目标字符串
- 模式
- 替换字符串
1 个可选参数:
- 替换次数
两个返回值:
- 替换后的字符串
- 替换的次数
1 |
|
10.1.4 string.gmatch
1 |
|
12 日期和时间
Lua 标准库提供了两个用于操作日期和时间的函数. 和 C 标准库中提供相同的功能.
14 数据结构
Lua 中的表并不是一种数据结构, 它们是其他数据结构的基础.
14.1 数组
如:
1 |
|
或:
1 |
|
这里其实可以把第一个索引值改为 0
, 但是这样的话 #
以及其他标准库会出问题.
16 编译, 执行和错误
虽然 Lua 是解释型语言 (interpreted language), 但其总是在运行代码前先预编译 (precompile) 源码为中间代码.
16.1 编译
dofile
是一个辅助函数, loadfile
完成核心工作, 其从文件中加载 Lua 代码段, 然它不会运行代码, 只是编译, 将编译后的代码段作为一个函数返回.
可以认为:
1 |
|
如果需要 多次运行同一个文件 , 只需调用一次 loadfile 函数后再多次调用它的返回结果即可. (只编译了一次, 开销小)
load
函数, 从一个字符串或函数中读取代码段:
1 |
|
之后, 变量 f
就会变成一个被调用时执行 i = i + 1
的函数:
1 |
|
不让其变为函数, 直接运行, 则利用其返回值:
1 |
|
load
函数总是在全局环境中编译代码段 如:
1 |
|
load
函数典型用法是执行外部代码 (用户输入的代码):
1 |
|
以下 load
使用和 loadfile
等价:
1 |
|
Lua 将所有独立的代码段当作匿名可编程参数的函数体, 如:
1 |
|
等价于:
1 |
|
load
和 loadfile
从来不引发错误, 当有错误发生时, 它们会返回 nil
及错误信息. 它们只是将程序段编译为一种中间形式, 然后将结果作为匿名函数返回.
实际上 Lua 中函数定义 是在运行时而不是在编译时发生的一种赋值操作.
16.2 预编译的代码
生成预编译文件 (即二进制文件):
1 |
|
执行:
1 |
|
几乎在 Lua 中所有能够使用源码的地方都可以使用预编译代码.
luac
的 -l
选项会列出编译器为指定代码段生成的操作码.
预编译形式的代码 不一定比源代码更小 , 但是却加载得快.
预编译形式的代码的另一个好处: 避免由于意外而修改源码.
16.3 错误
显式调用 error
函数并传入一个错误信息作为参数来引发一个错误:
1 |
|
也可用 assert
函数完成, 其检查第一个参数是否为真, 如果为真则返回该参数, 如果为假则引发第二个参数设置的错误信息:
1 |
|
当一个函数发现某种意外的情况, 在进行异常处理 (exception handling) 时可以采取两种基本方法:
- 返回错误代码, 如
nil
或false
- 调用
error
引发错误
原则: 容易避免的异常应该引发错误, 否则应该返回错误码.
16.4 错误处理和异常
处理错误, 使用 pcall
函数 (protected call) 来封装代码.
执行一段 Lua 代码并捕获 (try-catch) 执行中发生的所有错误, 首先将这段代码封装到一个函数中, 这个函数通常是一个匿名函数, 之后通过 pcall
来调用这个函数:
1 |
|
pcall
会以一种保护模式 (protected mode) 来调用它的第1个参数, 以便捕获该函数执行中的错误. 无论错误是否有错误发生, pcall
都不会引发错误. pcall
有 两个返回值:
- 没有错误, 返回
true
以及被调用函数的所有返回值 - 错误, 返回
false
及错误信息
16.5 错误信息和栈回溯
error
还有第 2 个可选参数 level
.
当 pcall
返回错误信息时, 部分的调用栈已经被破坏了 (从 pcall
到出错之处的部分), 如果希望得到一个有意义的栈回溯, 那么就必须在函数 pcall
返回前先将调用栈构造好, 即使用 xpcall
函数.
xpcall
和 pcall
类似, 但其第二个参数是一个消息处理函数 (message handler function)
当错误发生时, Lua 会在调用栈展开 (stack unwind) 前调用这个消息处理函数, 以便消息处理函数能够使用调试库来获取有关错误的更多信息.
两个常用的消息处理函数:
debug.debug
, 提供一个 Lua 提示符让用户来检查错误发生原因debug.traceback
, 使用调用栈来构造详细的错误信息
17 模块和包
从用户观点来看, 一个模块 (module) 就是一些代码 (要么是 Lua, 要么是 C), 这些代码可以通过函数 require
加载, 然后创建和返回一个表, 这个表就像是某种命名空间, 其中定义的内容是模块中导出的东西, 比如函数和常量.
如:
1 |
|
独立解释器会使用跟如下代码等价的方式 提前加载所有标准库
1 |
|
17.1 函数 require
首先, require
函数在表 package.loaded
中检查模块是否已被加载:
- 如果模块已经被加载, 函数
require
就返回相应的值。 - 如果模块未被加载, 函数
require
则搜索具有指定模块名的 Lua 文件, 搜索路径由变量package.path
指定.- 找到了, 就用
loadfile
加载, 返回一个称为加载器的函数 - 没找到, 搜索相应名称的 C 标准库, (路径由
package.cpath
指定) 找到则使用底层函数package.loadlib
进行加载, 这个底层函数会查找名为luaopen_modname
的函数并返回
- 找到了, 就用
为了最终加载模块 , require
函数带着两个参数调用加载函数 (大多数模块会忽略):
- 模块名
- 加载函数所在文件的名称
如果加载函数有返回值, 那么函数 require
会返回这个值, 然后保存在表 package.loaded
中.
如果加载函数没有返回值且表中的 package.loaded[@rep{modname}]
为空, 函数 require
就假设模块的返回值是 true
.
17.1.1 模块重命名
对于 Lua 语言模块: 写该 .lua
文件的文件名即可.
对于 C 标准库的二进制目标代码中 luaopen_*
函数的名称, 使用 连字符技巧 (见书, 不是很懂)
17.1.2 搜索路径
require
使用的路径是一组模板, 将 ?
替换为文件名来查找, 会一个一个尝试, 模板之间用 ;
分隔, 如一组模板为:
1 |
|
该值存储在 package.path
变量中.
则 require "sql"
会尝试打开:
sql
sql.lua
c:\windows\sql
/usr/local/lua/sql/sql.lua
当 package
模块被初始化后, 它就把变量 package.path
设置成环境变量 LUA_PATH_5_3
的值, 未定义则尝试 LUA_PATH
. 都未定义则使用一个编译时定义的默认路径.
在使用一个环境变量的值时, Lua 会将其中所有的 ;;
替换成默认路径, 例: LUA_PATH_5_3='mydir/?.lua;;'
, 则最终路径就会是模板 mydir/?.lua
后跟默认路径.
搜索 C 标准库类似. 变量为 package.cpath
, 环境变量为 LUA_CPATH_5_3
或 LUA_CPATH
17.1.3 搜索器
17.2 Lua 语言中编写模块的基本方法
创建模块的最简单方法是 创建一个表并将所有需要导出的函数放入其中, 最后返回这个表. 如:
1 |
|
不使用 return M
的方式:
1 |
|
require
会把模块的名称作为第一个参数传给加载函数, 因此这里的 ...
为模块名.
如果一个模块没有返回值, 那么 require
会返回 package.loaded[modname]
的当前值.
另一种编写模块的方法 : 把所有的函数定义为局部变量, 然后再最后返回表:
1 |
|
17.3 子模块和包
Lua 支持具有层次结构的模块名, 通过点来分隔名称中的层次.
当搜索一个定义子模块的文件时, require
会将点转换为另一个字符, 通常就是操作系统的目录分隔符.
20 元表和元方法
元表 是一个表, 其内容是 元方法 (metamethod)如 __add
, __sub
, 弄成表应该是方便查询. 可以修改一个值某个操作 (加法减法之类) 的行为, 如: a
和 b
都是表, 可以通过元表定义如何计算 a + b
当 Lua 语言试图将两个表相加, 它会先检查两者之一是否有元表 (metatable) 且该元表中是否有 __add
字段 (两个下划线). 如果 Lua 找到了该字段, 就调用该字段对应的值, 即 元方法 (metamethod) (一个函数).
元表只能给出预先定义的操作集合的行为, 其不支持继承.
Lua 中的每一个值都可以有元表.
用 getmetatable
来获取元表.
1 |
|
可以使用函数 setmetatable
来设置或修改任意表的元表:
1 |
|
Lua 中只能为表设置元表. 如果要为其他类型的值设置元表, 必须通过 C 代码或调试库完成. (但其他类型如 number, string 都有预定义的元表)
一个表可以成为任意值的元表.
20.1 算术运算相关的元方法
示例:
set
是一个类型的表
local mt = {}
是一个元表
setmetatable(set, mt)
设置 set
的元表为 mt
.
Set.union
定义了 set
这种类型表的相加操作
mt.__add = Set.union
设置元表的元方法.
之后 set
相加时如 set1 + set2
就会自动调用 mt.__add
即 Set.union
函数
每种算术运算符都有一个对应的元方法, 位操作也有元方法:
__sub
__div
__idiv
__unm
, 负数__mod
__pow
, 幂运算__band
, 按位与__bor
, 按位或__bxor
, 按位异或__bnot
, 按位取反__shl
, 向左移位__shr
, 向右移位__concat
, 连接运算符
等.
Lua 查找元方法的步骤 :
- 如果第一个值有元表且元表中存在所需的元方法, 那么 Lua 就使用这个元方法, 与第二个值无关
- 如果第二个值有元表且元表中存在所需的元方法, 那么 Lua 就使用这个元方法
- 都没有则抛出异常
20.2 关系运算相关的元方法
元表还允许指定关系运算符的含义, 其中的元方法包括:
__eq
等于__lt
小于__le
小于等于
其他三个关系运算符没有单独的元方法, 因为 Lua 会将 a~=b
转换为 not (a==b)
, a>b
转换为 b<a
, a>=b
转换为 b<=a
.
实现 __eq
, __le
和 __lt
示例:
1 |
|
20.3 库定义相关的元方法
Lua 虚拟机会检测一个操作中涉及的值是否有存在对应元方法的元表, 由于元表是一个普通表, 所以任何人都可以使用它们.
当对值进行格式化时, 函数 tostring
会首先检查值是否有一个元方法 __tostring
, 如果有, 就调用这个元方法, 将对象作为参数传给该函数, 然后把元方法的返回值作为函数 tostring
的返回值.
函数 setmetatable
和 getmetatable
用元方法来保护元表, 使用户既不能看到也不能修改元表. 需要在元表中设置 __metatable
字段, 则:
getmetatable
会返回这个字段的值setmetatable
会引发错误
1 |
|
当一个对象拥有 __pairs
元方法时, pair
会调用这个元方法来完成遍历.
20.4 表相关元方法
20.4.1 __index
元方法
当访问一个表中不存在的字段时会引发解释器查找一个名为 __index
的元方法, 如果没有这个元方法, 那么想一般情况下一样, 结果就是 nil
, 否则, 则有这个元方法来提供最终结果.
20.4.2 __newindex
元方法
与 __index
类似, 但其为当对一个表中不存在的索引赋值时, 解释器就会查找 __newindex
元方法. 如果这个元方法存在, 那么解释器就调用它而不执行赋值.
20.4.3 具有默认值的表
一个普通表中所有字段的默认值都是 nil
. 可通过元表修改, 如:
1 |
|
这个例子利用 __index
设置默认值, 这里传入的表 t
的默认值就是 d
.
20.4.4 跟踪对表的访问
由于 __index
和 __newindex
元方法都是在表中的索引不存在时才有用, 因此, 捕获对一个表所有访问的唯一方式是 保持表是空的.
如果要监控对一个表的所有访问, 需要为真正的表创建一个代理 (proxy). 这个代理是一个空的表, 具有用于跟踪所有访问并将访问重定向到原来的表的元方法.
见书.
20.4.5 只读的表
见书
21 面向对象 (Object-Oriented) 编程
从很多意义上讲, Lua 中的一张表就是一个对象.
在函数中使用全局名称 Account
是一种非常糟糕的编程习惯:
1 |
|
这里报错是因为 Account
变成 nil
, 而这里的 withdraw
函数绑定的是 Account
这个全局名称, 因此也无法调用.
正确做法为, 使用 self
, 作为一个接受者:
1 |
|
a1 = Account
就有了 a1.withdraw
, 调用 a1.withdraw(a1, 100)
时, self.balance
为 a1.balance
. 这样就能成功调用.
Lua 可以使用 冒号操作符 (colon operator) 隐藏该参数. 如:
1 |
|
21.1 类 (Class)
Lua 语言中没有类的概念. 可以基于原型的概念. (即一个模板)
如果有两个对象 A 和 B, 要让 B 成为 A 的一个原型:
1 |
|
{__index = B}
是 A
的元表, __index = B
是这个元表当前的内容. 当访问 A
的一个成员函数或成员变量时, A
中不存在就会根据 __index
的值返回 B
, 也就可以到 B
中查找.
一个类不仅可以提供方法, 还可以为示例中的字段提供常量和默认值:
1 |
|
Account:new()
返回的表绑定 Account
为元表.
21.2 继承 (Inheritance)
Account
类:
1 |
|
派生子类 SpecialAccount
则先创建从基类继承所有操作的空类:
1 |
|
可 重定义 从基类继承的任意方法, 毕竟是现在自己的表里找方法, 找不到再从继承的类中找, 如:
1 |
|
21.3 多重继承 (Multiple Inheritance)
多重继承也就是从多个类中继承.
__index
字段为一个函数时, 当 Lua 不能在原来的表中找到一个键时就会调用这个函数.
一种多重继承的实现:
1 |
|
21.4 私有性 (Privacy)
Lua 中标准的对象实现方式没有提供私有性机制.
一种常见的做法是把所有私有名称的最后加上一个下划线, 这样就能立刻区分出全局名称.
实现具有访问控制能力的对象, 基本思想是通过两个表来表示一个对象:
- 一个表用来保存对象的状态
- 另一个表用于保存对象的操作 (或接口)
我们通过第二个表来访问对象本身.
22 环境 (Environment)
可以认为, Lua 把所有全局变量保存在一个称为全局环境 (global environment) 的普通表中.
Lua 将全局环境自身保存在全局变量 _G
中 (_G._G
与 _G
等价).
输出全局环境中全部全局变量名称:
1 |
|
22.1 具有动态名称的全局变量
获取全局表中的值:
1 |
|
22.2 全局变量的声明
Lua 中的全局变量不需要声明就可以使用.
检测所有对全局表中不存在键的访问:
24 协程 (Coroutine)
协程可以颠倒调用者和被调用者的关系.
从多线程 (multithreading) 的角度看, 协程 (coroutine) 与线程 (thread) 类似.
协程是一系列的可执行语句, 拥有自己的栈, 局部变量和指令指针, 同时协程又与其他协程共享了全局变量和其他几乎一切资源.
线程和协程的主要区别在于 : 一个多线程程序可以并行运行多个线程, 而协程却需要彼此协作地运行. (毕竟在任意指定的时刻只能有一个协程运行, 且只有当正在运行的协程显式地要求被挂起 (suspend) 时其执行才会暂停)
24.1 协程基础
Lua 语言中协程相关的所有函数都被放在表 coroutine
中.
create()
创建新协程, 其只有一个参数, 即协程要执行的代码的函数. 其返回一个 thread
类型的值, 即新协程.
通常, create()
的参数是一个匿名函数:
1 |
|
协程有四种状态:
- 挂起 (suspended)
- 运行 (running)
- 正常 (normal)
- 死亡 (dead)
可以用 coroutine.status
来检查协程的状态:
1 |
|
当一个协程被创建时, 它处于挂起状态.
coroutine.resume
用于启动或在其启动一个协程的执行, 并将其状态由挂起改为运行:
1 |
|
resume
的其他参数为传入的参数.
协程的真正强大之处在于函数 yield
, 该函数可以让一个运行中的协程挂起自己, 然后再后续恢复运行:
1 |
|
这里加上 yield
的作用为, for
循环运行一次就再次挂起.
协程并不是一个无限循环的程序, 运行结束后就会 dead
, 就像这里的 for
循环结束后就会 dead
. 此时再唤醒就会报错.
像函数 pcall
一样, 函数 resume
也运行在保护模式中. 因此, 如果协程在执行中出错, Lua 语言不会显示错误信息, 而是将错误信息返回给函数 resume
.
当协程 A 唤醒协程 B 时, 协程 A 既不是挂起状态, 也不是运行状态, 因此此时的状态被称为 正常状态.
resume
的其他参数, 如:
1 |
|
coroutine.resume
的返回值中:
- 第一个返回值为
true
时表示没有错误 - 其他的返回值对应函数
yield
的参数
如:
1 |
|
这里 yield
的参数为 a+b
和 a-b
, 也就是其余的返回值.
coroutine.yield
的返回值是对应 resume
的参数, 即返回 resume
的参数 :
1 |
|
可以看出, 是 resume
的后面的参数.
当一个协程运行结束时, 主函数所返回的值 都将变成对应函数 resume
的返回值.
非对称协程 (asymmetric coroutine), 即用两个函数来控制协程的执行, 一个是用于挂起协程的执行, 另一个用于恢复协程的执行.
对称协程 (symmetric coroutine) , 即只提供一个函数用于在一个协程和另一个协程之间切换控制权.
24.2 哪个协程占据主循环
生产者-消费者问题:
1 |
|
具体见书.