Lua-程序设计-Notes

1 Lua 语言入门

1.1 程序段

程序段 (Chunk), 一组命令或表达式组成的序列.

-i 参数, 在执行完指定的程序段后进入交互模式:

1
$ lua -i prog

调用函数 dofile 可以加载一个文件:

1
2
$ lua
> dofile("lib1.lua")

就加载了 lib1.lua 文件.

1.2 一些词法规范

“下划线+大写字母” 组成的标识符通常被 Lua 语言用作特殊用途.

Lua 对大小写敏感.

-- 为单行注释.

长注释为:

1
2
3
--[[

]]

Lua 中连续语句之间的分隔符并不是必需的, 如果有需要的话可以使用分号来进行分隔, 以下等同:

1
2
3
4
5
6
7
8
9
a = 1
b = a * 2

a = 1;
b = a * 2;

a = 1; b = a * 2

a = 1 b = a * 2

1.3 全局变量

全局变量无需声明即可使用.

使用未初始化的全局变量不会报错, 得到 nil.

nil 赋值给全局变量, Lua 会回收该全局变量.

Lua 不区分未初始化变量和被赋值为 nil 的变量.

1.4 类型和值

基本类型有:

  • nil
  • boolean
  • number
  • string
  • userdata
  • function
  • thread
  • table

使用 type 函数查看:

1
type(nil)   --> nil

userdata 类型允许把任意的 C 语言数据保存在 Lua 语言变量中.

1.4.1 nil

nil 表示无效值.

1.4.2 Boolean

具有两个值:

  • true
  • false

可用逻辑运算符:

  • and, 如果它的第一个操作数为 false, 则返回第一个操作数, 否则返回第二个操作数
  • or, 如果它的第一个操作数不为 false, 则返回第一个操作数, 否则返回第二个操作数
  • not, 永远返回 Boolean 类型的值

1.5 独立解释器

在脚本开头添加:

1
#! /usr/local/bin/lua

不需要显式调用 Lua 语言解释器也可以直接运行 Lua 脚本.

-e 参数允许在命令行输入代码:

1
$ lua -e "print('Hello World')"

-l 参数用来加载库.

解释器在处理参数前, 会查找名为 LUA_INIT_5_3LUA_INIT 的环境变量

  • 如果变量内容为 @filename 则会运行相应文件
  • 如果变量存在但是不以 @ 开头, 解释器就会认为其包含 Lua 代码, 并会对其进行解释执行

编译器在运行代码前会创建一个名为 arg 的表, 其中存储了所有的命令行参数, 索引 0 保存脚本名, 索引 1 保存第一个参数:

1
$ lua script a b c

2 八皇后问题 (eight-queen puzzle)

其目标是把 8 个皇后合理地摆放在棋盘上, 让每个皇后之间都不能相互攻击.

3 数值

从 Lua 5.3 版本开始, Lua 语言为数值格式提供了两种选择:

  • 被称为 integer 的 64 位整型
  • 被称为 float 的双精度浮点类型

3.1 数值常量

具有十进制小数或者指数的数值会被当作浮点型值.

整型值和浮点型值的类型都是 number.

当需要区分整型值和浮点型值时, 使用函数 math.type.

// floor 除法运算符, 会将得到的商向负无穷取整, 从而保证结果是一个整数. 这样可以遵循一个规则: 如果操作数都是整型值, 那么结果就是整型值, 否则就是浮点型值.

3.3 关系运算

  • <
  • >
  • <=
  • >=
  • ==
  • ~=, 不等测试

3.4 数学库

标准数学库 math.

所有三角函数都以弧度为单位, 并通过函数 degrad 进行角度和弧度的转换.

math.random 用于生成伪随机数.

randomseed 用于设置伪随机数发生器的种子, 通常调用 math.randomseed(os.time())

3.4.2 取整函数

三个取整函数:

  • floor, 向负无穷取整
  • ceil, 向正无穷取整
  • modf, 向零取整, 除了返回取整后的值以外, 还会返回小数部分作为第二个结果

3.5 表示范围

最小值和最大值:

  • math.mininteger
  • math.maxinteger

3.6 惯例

通过增加 0.0 的方法将整型值强制转换为浮点型值:

1
2
$ lua
> -3 + 0.0 --> -3.0

3.7 运算符优先级

4 字符串

Lua 语言中的字符串是一串字节组成的序列.

字符使用 8 个比特位来存储.

Lua 语言的字符串是不可变值 (immutable value), 不能像 C 语言中那样直接修改某个字符串中的某个字符. 但可以通过创建一个新字符串的方式来达到修改的目的:

1
2
3
4
a = "one string"
b = string.gsub(a, "one", "another")
print(a)
print(b)

获取字符串长度, 使用 # 长度操作符:

1
2
3
a = "hello"
print(#a)
print(#"good bye")

返回字符串占用的字节数.

连接操作符 ..

4.1 字符串常量

双引号和单引号是等价的.

支持 C 语言风格的转义字符:

4.2 长字符串/多行字符串

使用一对双方括号来声明长字符串/多行字符串常量:

1
2
3
4
5
6
7
8
9
10
11
12
page = [[
<heml>
<head>
<title>An HTML Page</title>
</head>
<body>
<a hred="http://www.lua.org">Lua</a>
</body>
</html>
]]

write(page)

字符串中遇到 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
2
$ lua
> string.format("x = %d y = %d", 10, 20)

可用冒号操作符 : 直接调用:

1
2
3
4
$ lua
> string.sub(s,i,j)
> -- 等价于
> s:sub(i,j)

4.5 Unicode 编码

5 表 (table)

表 (table) 是 Lua 语言中最主要 (事实上也是唯一的) 数据结构.

调用函数 math.sin 对于 Lua 语言来说, 其实际含义是: 以字符串 “sin” 为键检索表 math.

5.1 表索引

同一个表中存储的值可以具有不同的类型索引, 并可以按需增长以容纳新的元素:

1
2
3
4
5
$ lua
> a = {}
> for i = 1, 1000 do a[i] = i*2 end
> a[9]
> a["x"] = 10

可以把表当结构体使用:

1
2
3
4
$ lua
> a.x = 10
> -- 等价于
> a["x"] = 10

5.2 表构造器

Table Constructor, 用来创建和初始化表的表达式.

空构造器: {}

初始化列表式:

1
2
3
days = {"Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"}

print(days[4]) --> Wednesday

初始化记录式 (record-like) 表:

1
a = {x = 10, y = 20}

无论使用哪种方式创建表, 都可以随时增加或删除表元素.

1
2
3
w = {x = 0, y = 0, label = "console"}
w.x = nil -- 删除
w[1] = "another field" 把键 1 增加到表 'w'

混用:

1
2
3
4
5
6
7
8
9
10
polyline = {color="blue",
thickness=2,
npoints=4,
{x=0, y=0}, -- polyline[1]
{x=-10, y=0}, -- polyline[2]
{x=-10, y=1}, -- polyline[3]
{x=0, y=1}, -- polyline[4]


}

更加通用的构造器, 通过方括号括起来的表达式显式指定每一个索引:

1
2
opnames = {["+"] = "add", ["-"] = "sub",
["*"] = "mul", ["/"] = "div"}

5.3 数组, 列表和序列

使用整型作为索引的表, 不需要预先声明表的大小:

1
2
3
4
a = {}
for i = 1, 10 do
a[i] l= io.read()
end

Lua 中, 数组的索引从 1 开始.

通常, 把列表的长度保存在表中某个非数值类型的字段中, 由于历史原因, 这个键通常是 “n”.

可以用 nil 值来标记列表的结束.

# 操作符也可以获取列表长度.

对于 Lua 语言而言, 一个为 nil 的字段和一个不存在的元素没有区别:

1
a = {10, 20, 30, nil, nil}

长度为 3 而不是 5.

处理存在空洞的列表时, 应该将列表的长度显式地保存起来.

5.4 遍历表

pairs 迭代器, 可得到表中的键值对:

1
2
3
4
t = {10, print, x = 12, k = "hi"}
for k, v in pairs(t) do
print(k, v)
end

受限于表在 Lua 语言中的底层实现机制, 遍历过程中元素的出现顺序可能是随机的.

对于列表 (上面的不算是列表) 可使用 ipairs 迭代器, 可以保证按照顺序.

for 循环遍历:

1
2
3
4
t = {10, print, 12, "hi"}
for k = 1, #t do
print(k, t[k])
end

5.5 安全访问

Lua 没有安全访问操作符.

a or {}anil 时其结果是一个空表, 对于表达式 (a or {}).b, 当 anil 时其结果也同样是 nil.

5.6 表标准库

table.insert

向序列指定位置插入一个元素, 不指定位置时在序列的最后插入:

1
2
3
t = [10, 20, 30]
table.insert(t,1,15)
-- t 变为 [15,10,20,30]

另一个例子:

1
2
3
4
5
t = {}
for line in io.lines() do
table.insert(t, line)
end
print(#t)

table.remove

删除并返回序列指定位置的元素, 然后将其后的元素向前豫东填充删除元素后造成的空洞.

不指定位置则删除最后一个.

1
2
table.remove(t,1)
table.remove(t)

table.move

1
table.move(a,f,e,t)

将表 a 中从索引 f 到 e 的元素移动到位置 t 上:

1
2
3
4
table.move(a, 1, #a, 2)
a[1] = newElement
table.move(a, 2, #a, 1)
a[#a] = nil

table.move 还支持使用一个表作为可选参数, 即将第一个表中的元素移动到第二个表中.

6 函数

无论哪种情况, 函数调用时都需要使用一对圆括号把参数列表括起来.

例外 当函数只有一个参数且该参数是字符串常量或表构造器:

1
2
3
4
5
6
print "Hello World"
dofile 'a.lua'
print [[a multi-line
message]]
f{x=10, y=20}
type{}

Lua 为面向对象提供的特殊语法, 冒号操作符 :

如:

1
o:foo(x)    -- 调用对象 o 的 foo 方法  

Lua 语言标准库中所有的函数都是使用 C 语言编写的.

函数语法格式 :

1
2
3
4
5
6
7
function add(a)
local sum = 0
for i = 1, #a do
sum = sum + a[i]
end
return sum
end

参数的行为与局部变量的行为完全一致, 相当于一个用函数调用时传入的值进行初始化的局部变量.

调用函数时使用的参数个数可以与定义函数时使用的参数不一致.

Lua 语言会通过抛弃多余参数和将不足的参数设为 nil 的方式来调整参数的个数.

默认参数:

1
2
3
4
function incCount(n)
n = n or 1
globalCounter = globalCounter + n
end

这里 n 的默认值相当于就是 1.

6.1 多返回值

Lua 允许一个函数返回多个结果.

return 后列出所有要返回的值即可:

如:

1
2
3
4
5
6
7
8
9
10
11
12
function maximum(a)
local mi = 1
local m = aa[mi]
for i = 1, #a do
if a[i] > m then
mi = i; m = a[i]
end
end
return m, mi
end

print(maximum({8,10,23,12,5}))

在多重赋值中, 如果一个函数没有返回值或者返回值个数不够多, 那么 Lua 语言会用 nil 来补充缺失的值:

1
x,y,z = foo()   -- x="a", y="b", z=nil

注意, 只有当函数调用是一系列表达式中的最后一个表达式时才能返回多值结果, 否则只能返回一个结果.

1
2
3
function foo() return "a", "b" end

x,y = foo(), 20 -- x="a", y=20

当一个函数调用是另一个函数调用的最后一个实参时, 第一个函数的所有返回值都会被作为实参传给第二个函数.

将函数调用用用一对圆括号括起来可以强制其只返回一个结果:

1
print((foo()))

6.2 可变长参数函数

如:

1
2
3
4
5
6
7
8
9
function add(...)
local s = 0
for _, v in ipairs{...} do
s = s + v
end
return s
end

print(add(3, 4, 10, 25, 12))

... 表示该函数的参数是可变长的.

当函数要访问这些参数时仍需用到 ...

表达式 {...} (被称为可变长参数表达式 vararg expression) 的结果是一个由所有可变长参数组成的列表.

可用变长参数来模拟 Lua 中普通的参数传递机制:

1
2
3
function foo(...)
local a, b, c = ...
end

格式化输出的函数 string.format. 输出文本函数 io.write

具有可变长参数的函数也可以具有任意数量的固定参数:

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

table.pack 函数, 保存所有参数, 然后将其放在一个表中返回, 这个表中还有一个保存了参数个数的额外字段 “n”.

判断可变长参数中是否包含无效的 nil:

1
2
3
4
5
6
7
function nonils(...)
local arg = table.pack(...)
for i = 1, arg.n do
if arg[i] == nil then return false end
end
return true
end

使用 select 函数遍历

函数 select 总是具有一个固定的参数 selector, 以及数量可变的参数, 如果 selector 是数值 n, 那么函数 select 则返回第 n 个参数后的所有参数, 否则, selector 应该是字符串 #, 以便函数 select 返回额外参数的总数:

1
2
3
4
print(select(1, "a", "b", "c"))     --> a b c
print(select(2, "a", "b", "c")) --> b c
print(select(3, "a", "b", "c")) --> c
print(select("#", "a", "b", "c")) --> 3

遍历如:

1
2
3
4
5
6
7
function add(...)
local s = 0
for i = 1, select("#", ...) do
s = s + select(i, ...)
end
return s
end

6.3 函数 table.unpack

其参数是一个数组, 返回值为数组内的所有元素.

1
2
print(table.unpack{10, 20, 30})
a,b = table.unpack{10, 20, 30}

通过数组 a 传入可变的参数来调用函数 f:

1
f(table.unpack(a))

两个等价形式:

1
print(string.find("hello", "ll"))

等价于:

1
2
3
4
f = string.find
a = {"hello", "ll"}

print(f(table.unpack(a)))

可以显示地限制返回元素的范围

1
print(table.unpack({"Sun", "Mon", "Tue", "Wed"}, 2, 3))

6.4 正确的尾调用

当一个函数的最后一个动作是调用另一个函数而没有进行其他工作时, 就形成了 尾调用 (tail call)

如:

1
2
3
4
function f(x)
x = x + 1
return g(x)
end

当 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
2
3
4
5
for count = 1, math.huge do
local line = io.read("L")
if line == nil then break end
io.write(string.format("%6d ", count), line)
end

使用 io.lines 迭代器, 逐行迭代一个文件:

1
2
3
4
local count = 0
for line in io.lines() do
count = count + 1
io.write(string.format("%6d ", count), line, "\n")

io.read(0) 是一个特例, 它常用于测试是否到达了文件末尾, 如果仍然有数据可供读取, 它会返回一个空字符串, 否则, 则返回 nil.

调用 read 时可以指定多个选项, 函数会根据每个参数返回相应的结果, 如:

1
2
3
4
5
while true do
local n1, n2, n3 = io.read("n", "n", "n")
if not n1 then break end
print(math.max(n1, n2, n3))
end

7.2 完整 I/O 模型

io.open

两个参数:

  • 待打开的文件名
  • 模式, 包括: 只读的 r, 只写的 w, 追加的 a, 打开二进制文件的 b

检查错误, 使用 assert 函数:

1
local f = assert(io.open(filename, mode))

错误信息会作为函数 assert 的第二个参数被传入, 之后函数 assert 会将错误信息展示出来.

在打开文件后, 可以使用方法 readwrite 从流中读取和向流中写入, 需要 使用冒号运算符 将它们当作流对象的方法来调用:

1
2
3
local f = assert(io.open(filename, "r"))
local t = f:read("a")
f:close()

三个预定义的 C 语言流的句柄:

  • io.stdin
  • io.stdout
  • io.stderr

混用 I/O 模型:

1
2
3
4
local temp = ioo.input()
io.input("newinput")
io.input():close()
io.input(temp)

注意:

  • 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
2
3
4
5
6
7
8
local x1, x2
do
local a2 = 2*a
local d = (b^2 - 4*a*c)^(1/2)
x1 = (-b + d)/a2
x2 = (-b - d)/a2
end
print(x1, x2)

尽可能使用局部变量是一种良好的编程风格.

Lua 语言的发行版中有一个用于全局变量检查的模块 strict.lua, 如果试图在一个函数中对不存在的全局变量赋值或者使用不存在的全局变量, 将会抛出异常.

8.2 控制结构

所有的控制结构语法上都有一个显式的终结符: end

注意, Lua 语言将所有不是 falsenil 的值当作真.

8.2.1 if then else

1
2
3
4
5
6
7
8
9
10
11
if op == "+" then
r = a + b
elseif op == "-" then
r = a - b
elseif op == "*" then
r = a*b
elseif op == "/" then
r = a/b
else
error("invalid operation")
end

then 表达式可以作为条件表达式的终止标记.

error 函数用于报错.

Lua 不支持 switch 语句.

8.2.2 while

1
2
3
4
5
local i = 1
while a[i] do
print(a[i])
i = i + 1
end

8.2.3 repeat

1
2
3
4
5
local line 
repeat
line = io.read()
until line ~= ""
print(line)

8.2.4 数值型 for

两种类型的 for 语句:

  • 数值型 (numerical)
  • 泛型 (generic)

数值型如:

1
2
3
for var = exp1, exp2, exp3 do
something
end

var 的值从 exp1 变化到 exp2, exp3 是步长 (step) 可选, 默认为 1.

不设上限, 使用 math.huge:

1
2
3
4
5
6
for i = 1, math.huge do
if (0.3*i^3 - 20*i^2 - 500 >= 0) then
print(i)
break
end
end

这里的控制变量 i 是局部变量.

注意, 不要改变控制变量的值, 随意改变控制变量的值可能产生不可预知的结果.

8.2.5 泛型 for

泛型 for 遍历迭代函数返回的所有值. (如 pairs, ipairs, io.lines)

泛型 for 可以使用多个变量, 这些变量在每次循环时都会更新, 当第一个变量变为 nil 循环终止.

1
2
3
4
t = {10, print, x = 12, k = "hi"}
for k, v in pairs(t) do
print(k, v)
end

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
2
a = {p = print}     -- 'a.p' 指向 'print' 函数
a.p("Hello World") -- Hello World

函数的定义可以由:

1
2
3
function foo(x) 
return 2*x
end

写成:

1
2
3
foo = function (x)
return 2*x
end

注意, 在 Lua 中所有函数都是匿名的 (anonynous), 即没有名字, 当讨论函数名如 print 时, 实际上指的是保存该函数的变量.

高阶函数, 以另一个函数作为参数, 如 sort:

1
table.sort(network, function (a,b) return (a.name > b.name) end)

9.2 非全局函数

表字段中存储函数, 有很多中写法:

1
2
3
lib = {}
lib.foo = function (x,y) return x + y end
lib.goo = function (x,y) return x - y end

或:

1
2
3
4
lib = {
foo = function (x,y) return x + y end,
goo = function (x,y) return x - y end
}

或:

1
2
3
lib = {}
function lib.foo (x,y) return x + y end
function lib.goo (x,y) return x - y end

当把一个函数存储到局部变量时, 就得到了一个局部函数 (local function).

定义局部函数:

1
2
3
local function f(params)
body
end

其展开为:

1
2
3
local f; f = functino (params)
body
end

9.3 词法定界

闭包 (closure). 即一个持有外部环境变量的函数就是闭包.

由于函数可以被保存在普通变量中, 因此 Lua 语言中可以轻松地重定义函数:

1
2
3
4
5
6
7
do 
local oldSin = math.sin
local k = math.pi / 180
math.sin = function (x)
return oldSin(x * k)
end
end

10 模式匹配

10.1 模式匹配相关函数

字符串标准库提供的 4 个函数:

  • find
  • gsub (global substitute)
  • match
  • gmatch (global match)

10.1.1 string.find

两个参数:

  • 字符串
  • 要匹配的值

两个可选参数:

  • 第三个, 整数, 从那里开始的索引
  • 第四个, 布尔值, 是否进行简单搜索

匹配成功返回两个值:

  • 模式开始位置的索引
  • 模式结束位置的索引

没找到任何匹配, 返回 nil.

1
2
s = "hello world"
i, j = string.find(s, "hello")

10.1.2 string.match

string.find 类似, 但是返回字符串.

1
print(string.match("hello world", "hellow"))    --> hello

10.1.3 string.gsub

3 个必选参数:

  • 目标字符串
  • 模式
  • 替换字符串

1 个可选参数:

  • 替换次数

两个返回值:

  • 替换后的字符串
  • 替换的次数
1
2
s = string.gsub("Lua is cute", "cute", "great")
print(s) --> Lua is great

10.1.4 string.gmatch

1
2
3
4
5
s = "some string"
words = {}
for w in string.gmatch(s, "%a+") do
words[#words + 1] = w
end

12 日期和时间

Lua 标准库提供了两个用于操作日期和时间的函数. 和 C 标准库中提供相同的功能.

14 数据结构

Lua 中的表并不是一种数据结构, 它们是其他数据结构的基础.

14.1 数组

如:

1
2
3
4
local a = {}
for i = 1, 10 do
a[i] = 0
end

或:

1
squares = {1, 4, 9, 16}

这里其实可以把第一个索引值改为 0, 但是这样的话 # 以及其他标准库会出问题.

16 编译, 执行和错误

虽然 Lua 是解释型语言 (interpreted language), 但其总是在运行代码前先预编译 (precompile) 源码为中间代码.

16.1 编译

dofile 是一个辅助函数, loadfile 完成核心工作, 其从文件中加载 Lua 代码段, 然它不会运行代码, 只是编译, 将编译后的代码段作为一个函数返回.

可以认为:

1
2
3
4
function dofile(filename)
local f = assert(loadfile(filename))
return f()
end

如果需要 多次运行同一个文件 , 只需调用一次 loadfile 函数后再多次调用它的返回结果即可. (只编译了一次, 开销小)

load 函数, 从一个字符串或函数中读取代码段:

1
f = load("i = i + 1")

之后, 变量 f 就会变成一个被调用时执行 i = i + 1 的函数:

1
2
3
i = 0
f(); print(i) --> 1
f(); print(i) --> 2

不让其变为函数, 直接运行, 则利用其返回值:

1
2
3
load(i = i + 1)()
-- 最好写为
assert(load(s))()

load 函数总是在全局环境中编译代码段 如:

1
2
3
4
5
6
i = 32
local i = 0
f = load("i = i + 1; print(i)")
g = function () i = i + 1; print(i) end
f() --> 33
g() --> 1

load 函数典型用法是执行外部代码 (用户输入的代码):

1
2
3
4
print "enter your expression"
local line = io.read()
local func = assert(load("return " .. line))
print("the value of your expression is " .. func())

以下 load 使用和 loadfile 等价:

1
f = load(io.lines(filename, 1024))

Lua 将所有独立的代码段当作匿名可编程参数的函数体, 如:

1
load("a=1")

等价于:

1
function (...) a - 1 end

loadloadfile 从来不引发错误, 当有错误发生时, 它们会返回 nil 及错误信息. 它们只是将程序段编译为一种中间形式, 然后将结果作为匿名函数返回.

实际上 Lua 中函数定义 是在运行时而不是在编译时发生的一种赋值操作.

16.2 预编译的代码

生成预编译文件 (即二进制文件):

1
$ luac -o prog.lc prog.lua

执行:

1
$ lua prog.lc

几乎在 Lua 中所有能够使用源码的地方都可以使用预编译代码.

luac-l 选项会列出编译器为指定代码段生成的操作码.

预编译形式的代码 不一定比源代码更小 , 但是却加载得快.

预编译形式的代码的另一个好处: 避免由于意外而修改源码.

16.3 错误

显式调用 error 函数并传入一个错误信息作为参数来引发一个错误:

1
2
3
print "enter a number:"
n = io.read("n")
if not n then error("invalid input") end

也可用 assert 函数完成, 其检查第一个参数是否为真, 如果为真则返回该参数, 如果为假则引发第二个参数设置的错误信息:

1
2
print "enter a number:"
n = assert(io.read("*n"), "invalid input")

当一个函数发现某种意外的情况, 在进行异常处理 (exception handling) 时可以采取两种基本方法:

  • 返回错误代码, 如 nilfalse
  • 调用 error 引发错误

原则: 容易避免的异常应该引发错误, 否则应该返回错误码.

16.4 错误处理和异常

处理错误, 使用 pcall 函数 (protected call) 来封装代码.

执行一段 Lua 代码并捕获 (try-catch) 执行中发生的所有错误, 首先将这段代码封装到一个函数中, 这个函数通常是一个匿名函数, 之后通过 pcall 来调用这个函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
local ok, meg = pcall(function ()

...

if unexpected_condition then error() end
some code
print(a[i])

...

end)

if ok then
regular code
else
error-handing code
end

pcall 会以一种保护模式 (protected mode) 来调用它的第1个参数, 以便捕获该函数执行中的错误. 无论错误是否有错误发生, pcall 都不会引发错误. pcall两个返回值:

  • 没有错误, 返回 true 以及被调用函数的所有返回值
  • 错误, 返回 false 及错误信息

16.5 错误信息和栈回溯

error 还有第 2 个可选参数 level.

pcall 返回错误信息时, 部分的调用栈已经被破坏了 (从 pcall 到出错之处的部分), 如果希望得到一个有意义的栈回溯, 那么就必须在函数 pcall 返回前先将调用栈构造好, 即使用 xpcall 函数.

xpcallpcall 类似, 但其第二个参数是一个消息处理函数 (message handler function)

当错误发生时, Lua 会在调用栈展开 (stack unwind) 前调用这个消息处理函数, 以便消息处理函数能够使用调试库来获取有关错误的更多信息.

两个常用的消息处理函数:

  • debug.debug, 提供一个 Lua 提示符让用户来检查错误发生原因
  • debug.traceback, 使用调用栈来构造详细的错误信息

17 模块和包

从用户观点来看, 一个模块 (module) 就是一些代码 (要么是 Lua, 要么是 C), 这些代码可以通过函数 require 加载, 然后创建和返回一个表, 这个表就像是某种命名空间, 其中定义的内容是模块中导出的东西, 比如函数和常量.

如:

1
2
local m = require "math"
print(m.sin(3.14))

独立解释器会使用跟如下代码等价的方式 提前加载所有标准库

1
2
3
math = require "math"
string = require "string"
...

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
?;?.lua;c:\windows\?;/usr/local/lua/?/?.lua

该值存储在 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_3LUA_CPATH

17.1.3 搜索器

17.2 Lua 语言中编写模块的基本方法

创建模块的最简单方法是 创建一个表并将所有需要导出的函数放入其中, 最后返回这个表. 如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
local M = {}    -- 模块

-- 创建一个新的复数
local function new(r, i)
return {r=r, i=i}
end

M.new = new -- 把 'new' 加到模块中

-- constant 'i'
M.i = new(0, 1)

function M.add(c1, c2)
return new(c1.r + c2.r, c1.i + c2.i)
end

...

return M

不使用 return M 的方式:

1
2
local M = {}
package.loaded[...] = M

require 会把模块的名称作为第一个参数传给加载函数, 因此这里的 ... 为模块名.

如果一个模块没有返回值, 那么 require 会返回 package.loaded[modname] 的当前值.

另一种编写模块的方法 : 把所有的函数定义为局部变量, 然后再最后返回表:

1
2
3
4
5
6
7
8
9
10
11
function add(c1, c2)
return new(c1.r + c2.r, c1.i + c2.i)
end

...

return {
new = new,
add = add,
...
}

17.3 子模块和包

Lua 支持具有层次结构的模块名, 通过点来分隔名称中的层次.

当搜索一个定义子模块的文件时, require 会将点转换为另一个字符, 通常就是操作系统的目录分隔符.

20 元表和元方法

元表 是一个表, 其内容是 元方法 (metamethod)__add, __sub, 弄成表应该是方便查询. 可以修改一个值某个操作 (加法减法之类) 的行为, 如: ab 都是表, 可以通过元表定义如何计算 a + b

当 Lua 语言试图将两个表相加, 它会先检查两者之一是否有元表 (metatable) 且该元表中是否有 __add 字段 (两个下划线). 如果 Lua 找到了该字段, 就调用该字段对应的值, 即 元方法 (metamethod) (一个函数).

元表只能给出预先定义的操作集合的行为, 其不支持继承.

Lua 中的每一个值都可以有元表.

getmetatable 来获取元表.

1
2
t = {}
print(getmetatable(t)) --> nil

可以使用函数 setmetatable 来设置或修改任意表的元表:

1
2
3
t1 = {}
setmetatable(t, t1)
print(getmetatable(t) == t1) --> true

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.__addSet.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
2
3
4
5
6
7
8
9
10
11
12
13
14
mt.__eq = function (a, b)
return a <=b and b <= a
end

mt.__le = function (a, b)
for k in pairs(a) do
if not b[k] then return false end
end
return true
end

mt.__lt = function (a, b)
return a <= b and not (b <= a)
end

20.3 库定义相关的元方法

Lua 虚拟机会检测一个操作中涉及的值是否有存在对应元方法的元表, 由于元表是一个普通表, 所以任何人都可以使用它们.

当对值进行格式化时, 函数 tostring 会首先检查值是否有一个元方法 __tostring, 如果有, 就调用这个元方法, 将对象作为参数传给该函数, 然后把元方法的返回值作为函数 tostring 的返回值.

函数 setmetatablegetmetatable 用元方法来保护元表, 使用户既不能看到也不能修改元表. 需要在元表中设置 __metatable 字段, 则:

  • getmetatable 会返回这个字段的值
  • setmetatable 会引发错误
1
mt.__metatable = "not your business"

当一个对象拥有 __pairs 元方法时, pair 会调用这个元方法来完成遍历.

20.4 表相关元方法

20.4.1 __index 元方法

当访问一个表中不存在的字段时会引发解释器查找一个名为 __index 的元方法, 如果没有这个元方法, 那么想一般情况下一样, 结果就是 nil, 否则, 则有这个元方法来提供最终结果.

20.4.2 __newindex 元方法

__index 类似, 但其为当对一个表中不存在的索引赋值时, 解释器就会查找 __newindex 元方法. 如果这个元方法存在, 那么解释器就调用它而不执行赋值.

20.4.3 具有默认值的表

一个普通表中所有字段的默认值都是 nil. 可通过元表修改, 如:

1
2
3
4
5
6
function setDefault (t, d)
local mt = {__index = function () return d end}
setmetatable(t, mt)
end

tab = {x=10, y=20}

这个例子利用 __index 设置默认值, 这里传入的表 t 的默认值就是 d.

20.4.4 跟踪对表的访问

由于 __index__newindex 元方法都是在表中的索引不存在时才有用, 因此, 捕获对一个表所有访问的唯一方式是 保持表是空的.

如果要监控对一个表的所有访问, 需要为真正的表创建一个代理 (proxy). 这个代理是一个空的表, 具有用于跟踪所有访问并将访问重定向到原来的表的元方法.

见书.

20.4.5 只读的表

见书

21 面向对象 (Object-Oriented) 编程

从很多意义上讲, Lua 中的一张表就是一个对象.

在函数中使用全局名称 Account 是一种非常糟糕的编程习惯:

1
2
3
4
5
6
7
Account = { balance = 0 }
function Account.withdraw(v)
Account.balance = Account.balance -v
end

a, Account = Account, nil
a.withdraw(100.00) -- ERROR!

这里报错是因为 Account 变成 nil, 而这里的 withdraw 函数绑定的是 Account 这个全局名称, 因此也无法调用.

正确做法为, 使用 self, 作为一个接受者:

1
2
3
4
5
6
7
8
function Account.withdraw(self, v)
self.balance = self.balance - v
end

a1, Account = Account, nil
a1.withdraw(a1, 100) -- OK

a2 = { balance = 0, withdraw = Account.withdraw}

a1 = Account 就有了 a1.withdraw, 调用 a1.withdraw(a1, 100) 时, self.balancea1.balance. 这样就能成功调用.

Lua 可以使用 冒号操作符 (colon operator) 隐藏该参数. 如:

1
2
3
4
5
6
7
a2.withdraw(a2, 100.00)
-- 等价于
a2:withdraw(100.00)
-- 也可以定义时
function Account:withdraw(v)
self.balance = self.balance - v
end

21.1 类 (Class)

Lua 语言中没有类的概念. 可以基于原型的概念. (即一个模板)

如果有两个对象 A 和 B, 要让 B 成为 A 的一个原型:

1
setmetatable(A, {__index = B})

{__index = B}A 的元表, __index = B 是这个元表当前的内容. 当访问 A 的一个成员函数或成员变量时, A 中不存在就会根据 __index 的值返回 B, 也就可以到 B 中查找.

一个类不仅可以提供方法, 还可以为示例中的字段提供常量和默认值:

1
2
3
4
5
6
7
8
9
function Account:new(o)
o = o or {}
self.__index = self
setmetatable(o. self)
return o
end

b = Account:new()
print(b.balance) --> 0

Account:new() 返回的表绑定 Account 为元表.

21.2 继承 (Inheritance)

Account 类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
Account = {balance = 0}

function Account:new(o)
o = o or {}
self.__index = self
setmetatable(o, self)
return o
end

function Account:deposit(v)
self.balance = self.balance + v
end

function Account:withdraw(v)
if v > self.balance then error "insufficient funds" end
self.balance = self.balance - v
end

派生子类 SpecialAccount 则先创建从基类继承所有操作的空类:

1
SpecialAccount = Account:new()

重定义 从基类继承的任意方法, 毕竟是现在自己的表里找方法, 找不到再从继承的类中找, 如:

1
2
3
4
5
6
7
8
9
function SpecialAccount:withdraw(v)
if v - self.balance >= self:getLimit() then
error "insufficient funds"
end
end

function SpecialAccount:getLimit()
return self.limit or 0
end

21.3 多重继承 (Multiple Inheritance)

多重继承也就是从多个类中继承.

__index 字段为一个函数时, 当 Lua 不能在原来的表中找到一个键时就会调用这个函数.

一种多重继承的实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
local function search(k, plist)
for i = 1, #plist do
local v = plist[i][k] -- 尝试第 'i' 个超类
if v then return v end
end
end

function createClass(...) -- 参数为要继承的类
local c = {} -- 新类
local parents = {...} -- 父类列表

-- 在父类列表中查找类缺失的方法
setmetatable(c, {__index = function(t, k)
return search(k, parents)
end})

-- 将 'c' 作为其实例的元表
c.__index = c

-- 为新类定义一个新的构造函数
function c:new(o)
o = o or {}
setmetatable(o, c)
return o
end

return c -- 返回新类
end

21.4 私有性 (Privacy)

Lua 中标准的对象实现方式没有提供私有性机制.

一种常见的做法是把所有私有名称的最后加上一个下划线, 这样就能立刻区分出全局名称.

实现具有访问控制能力的对象, 基本思想是通过两个表来表示一个对象:

  • 一个表用来保存对象的状态
  • 另一个表用于保存对象的操作 (或接口)

我们通过第二个表来访问对象本身.

22 环境 (Environment)

可以认为, Lua 把所有全局变量保存在一个称为全局环境 (global environment) 的普通表中.

Lua 将全局环境自身保存在全局变量 _G 中 (_G._G_G 等价).

输出全局环境中全部全局变量名称:

1
for n in pairs (_G) do print(n) end

22.1 具有动态名称的全局变量

获取全局表中的值:

1
value = _G[varname]

22.2 全局变量的声明

Lua 中的全局变量不需要声明就可以使用.

检测所有对全局表中不存在键的访问:

24 协程 (Coroutine)

协程可以颠倒调用者和被调用者的关系.

从多线程 (multithreading) 的角度看, 协程 (coroutine) 与线程 (thread) 类似.

协程是一系列的可执行语句, 拥有自己的栈, 局部变量和指令指针, 同时协程又与其他协程共享了全局变量和其他几乎一切资源.

线程和协程的主要区别在于 : 一个多线程程序可以并行运行多个线程, 而协程却需要彼此协作地运行. (毕竟在任意指定的时刻只能有一个协程运行, 且只有当正在运行的协程显式地要求被挂起 (suspend) 时其执行才会暂停)

24.1 协程基础

Lua 语言中协程相关的所有函数都被放在表 coroutine 中.

create() 创建新协程, 其只有一个参数, 即协程要执行的代码的函数. 其返回一个 thread 类型的值, 即新协程.

通常, create() 的参数是一个匿名函数:

1
2
co = coroutine.create(function () print("hi") end)
print(type(co))

协程有四种状态:

  • 挂起 (suspended)
  • 运行 (running)
  • 正常 (normal)
  • 死亡 (dead)

可以用 coroutine.status 来检查协程的状态:

1
print(coroutine.status(co))

当一个协程被创建时, 它处于挂起状态.

coroutine.resume 用于启动或在其启动一个协程的执行, 并将其状态由挂起改为运行:

1
coroutine.resume(co)

resume 的其他参数为传入的参数.

协程的真正强大之处在于函数 yield, 该函数可以让一个运行中的协程挂起自己, 然后再后续恢复运行:

1
2
3
4
5
6
7
8
co = coroutine.create(function ()
for i = 1, 10 do
print("co", i)
coroutine.yield()
end
end)

print(coroutine.status(co))

这里加上 yield 的作用为, for 循环运行一次就再次挂起.

协程并不是一个无限循环的程序, 运行结束后就会 dead, 就像这里的 for 循环结束后就会 dead. 此时再唤醒就会报错.

像函数 pcall 一样, 函数 resume 也运行在保护模式中. 因此, 如果协程在执行中出错, Lua 语言不会显示错误信息, 而是将错误信息返回给函数 resume.

当协程 A 唤醒协程 B 时, 协程 A 既不是挂起状态, 也不是运行状态, 因此此时的状态被称为 正常状态.

resume 的其他参数, 如:

1
2
3
4
5
co = coroutine.create(function (a, b, c)
print("co", a, b, c + 2)
end)

coroutine.resume(co, 1, 2, 3)

coroutine.resume 的返回值中:

  • 第一个返回值为 true 时表示没有错误
  • 其他的返回值对应函数 yield 的参数

如:

1
2
3
4
5
co = coroutine.create(function (a, b)
coroutine.yield(a+b, a-b)
end)

print(coroutine.resume(co, 20, 10)) --> true 30 10

这里 yield 的参数为 a+ba-b, 也就是其余的返回值.

coroutine.yield 的返回值是对应 resume 的参数, 即返回 resume 的参数 :

1
2
3
4
5
6
7
co = coroutine.create(function (x)
print("co1", x)
print("co2", coroutine.yield())
end)

coroutine.resume(co, "hi") --> co1 hi
coroutine.resume(co, 4, 5) --> co2 4 5

可以看出, 是 resume 的后面的参数.

当一个协程运行结束时, 主函数所返回的值 都将变成对应函数 resume 的返回值.

非对称协程 (asymmetric coroutine), 即用两个函数来控制协程的执行, 一个是用于挂起协程的执行, 另一个用于恢复协程的执行.

对称协程 (symmetric coroutine) , 即只提供一个函数用于在一个协程和另一个协程之间切换控制权.

24.2 哪个协程占据主循环

生产者-消费者问题:

1
2
3
4
5
6
7
8
9
10
11
12
13
function producer ()
while true do
local x = io.read() -- 产生新值
send(x) -- 发给消费者
end
end

function consumer ()
while true do
local x = receive() -- 接收来自生产者的值
io.write(x, "\n") -- 消费
end
end

具体见书.

24.3 将协程用作迭代器


Lua-程序设计-Notes
http://example.com/2022/12/29/Lua-程序设计-Notes/
作者
Jie
发布于
2022年12月29日
许可协议