Chapter1 Lua语言入门

1.3 全局变量

在Lua语言中,全局变量(Global Variable)无须声明即可使用,使用未经初始化的全局变量也不会导致错误。
当使用未经初始化的全局变量时,得到的结果是nil:

1
2
3
> b			--> nil
> b = 10
> b --> 10

当把nil赋值给全局变量时,Lua会回收该全局变量(就像该全局变量从来没有出现过一样),例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
> b = nil 
> b --> nil
```
Lua语言不区分未初始化变量和被赋值为nil的变量。
在上述赋值语句执行后,Lua语言会最终回收该变量占用的内存。
## 1.4 类型和值
Lua语言是一种动态类型语言(Dynamically-typed language),在这种语言中没有类型定义(type definition),每个值都带有其自身的类型信息。
Lua语言中有8种基本类型:nil(空)、boolean(布尔)、number(数值)、string(字符串)、userdata(用户数据)、function(函数)、thread(线程)、table(表)。
使用函数type可获取一个值对应的类型名称:
```Lua
> type(nil) --> nil
> type(true) --> boolean
> type(10.4 * 3) --> number
> type("Hello world") --> string
> type(io.stdin) --> userdata
> type(print) --> function
> type(type) --> thread
> type({}) --> table
> type(type(X)) --> string

不管X是什么,最后一行返回的永远是”string”。
这是因为函数type的返回值永远是一个字符串。

userdata类型允许把任意的C语言数据保存在Lua语言变量中。
在Lua语言中,用户数据类型除了赋值和相等性测试外,没有其他预定义的操作。
用户数据被用来表示由应用或C语言编写的库所创建的新类型。
例如,标准I/O库使用用户数据来表示打开的文件。
我们会在后面涉及CAPI时再讨论更多的相关内容。

变量没有预定义的类型,任何变量都可以包含任何类型的值:

1
2
3
4
5
6
7
> type(a)					--> nil
> a = 10
> type(a) --> number
> a = "a string!!"
> type(a) --> string
> a = nil
> type(a) --> nil

一般情况下,将一个变量用作不同类型时会导致代码的可读性不佳;但是,在某些情况下谨慎地使用这个特性可能会带来一定程度的便利。

Boolean

在Lua语言中,条件测试(例如控制结构中的分支语句)将除Boolean值false和nil外的所有其他值视为真。特别的是,在条件检测中Lua语言把零和空字符串也都视为真。

Lua语言支持常见的逻辑运算符:and 、or 和 not。和条件测试一样,所有的逻辑运算将Boolean类型的 false 和 nil 当做假,而把其他值当作真。

逻辑运算符 and 的运算结果为:如果它的第一个操作数为“false”,则返回第一个操作数,否则返回第二个操作数。
逻辑运算符 or 的运算结果为:如果它的第一个操作数不为“false”,则返回第一个操作数,否则返回第二个操作数。
例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
> 4 and 5					--> 5
> nil and 13 --> nil
> false and 13 --> false
> 0 or 5 --> 0
> false or "hi" --> "hi"
> nil or false --> false
```
andor 都遵循短路求值(Short-circuit evaluation)原则,即只在必要时才对第二个操作数进行求值。
例如,根据短路求值的原则,表达式(i~=0 and a/i>b)不会发生运行时异常(当i等于0时,a/i不会执行)。

在Lua语言中,形如x=x or v的惯用写法非常有用,它等价于:
```Lua
if not x then
x = v;
end

即,当x未被初始化时,将其默认值设为v(假设x不是Boolean类型的false。)

另一种有用的表达式形如((a and b)or c)或(a and b or c)(由于 and 的运算符优先级高于 or,所以这两种表达形式等价,后面会详细介绍),当b不为false时,它们还等价于C语言的三目运算符a?b:c。
例如,我们可以使用表达式(x>y)and x or y选出数值x和y中较大的一个。当x>y时,and 的第一个操作数为true,与第二个操作数(x)进行 and 运算后结果为x,最终与 or 运算后返回第一个操作数x。当x>y不成立时,and 表达式的值为false,最终 or 运算后的结果是第二个操作数y。

not 运算符永远返回Boolean类型的值:

1
2
3
4
5
> not nil					--> true
> not false --> true
> not 0 --> false
> not not 1 --> true
> not not nil --> false

Chapter2 小插曲:八皇后问题

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
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
N = 8;	--棋盘大小

--检查(n,c)是否不会被攻击
function IsPlaceOk(a,n,c)
for i = 1,n - 1 do --对于每一个已经被放置的皇后
if a[i] then
if (a[i] == c) or --同一列?
(a[i] - i == c - n) or --同一对角线?
(a[i] + i == c + n) then
return false --位置会被攻击
end
end
end

return true --不会被攻击;位置有效
end

--打印棋盘
function PrintSolution(a)
for i = 1,N do --对于每一行
for j = 1,N do --和每一列
--输出“X”或“-”,外加一个空格
io.write(a[i] == j and "X" or "-"," ")
end
io.write("\n");
end
io.write("\n");
end

--把从“n”到“N”的所有皇后放在棋盘“a”上
function AddQueen(a,n)
if n > N then --是否所有的皇后都被放置好了
PrintSolution(a);
else --尝试着放置第n个皇后
for c = 1,N do
if IsPlaceOk(a,n,c) then
a[n] = c --把第n个皇后放在列“c”
AddQueen(a,n+1);
end
end
end
end

AddQueen({},1);

Chapter3 数值

3.1数值常量

具有十进制小数或者指数的数值会被当作浮点型值,否则会被当作整型值。
整型值和浮点型值的类型都是”number”:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
> type(3)	--> number
> type(3.5) --> number
> type(3.0) --> number
```
由于整型值和浮点型值的类型都是"number",所以它们是可以相互转换的。
同时,具有相同算术值的整型值和浮点型值在Lua语言中是相等的:
```c
> 1 == 1.0 --> true
> -3 == -3.0 --> true
> 0.2e3 == 200 --> true
```
在少数情况下,当需要区分整型值和浮点型值时,可以使用函数math.type:
```c
> math.type(3) --> integer
> math.type(3.0) --> float

3.2算术运算

对于Lua 5.3中引入的整型而言,主要的建议就是“开发人员要么选择忽略整型和浮点型二者之间的不同,要么就完整地控制每一个数值的表示。”
因此,所有的算术操作符不论操作整型值还是浮点型值,结果都应该是一样的。

两个整型值进行相加、相减、相乘、相除和取负操作的结果仍然是整型值。
对于这些算术运算而言,操作数是用整型还是用浮点型表示的整数都没有区别(除非发生溢出,参见3.5节):

1
2
> 13 + 15	--> 28
> 13.0 + 15.0 --> 28.0

如果两个操作数都是整型值,那么结果也是整型值;否则,结果就是浮点型值。
当操作数一个是整型值一个是浮点型值时,Lua语言会在进行算术运算前先将整型值转换为浮点型值:

1
2
> 13.0 + 25		--> 38.0
> -(3 * 6.0) --> -18.0

由于两个整数相除的结果并不一定是整数(数学领域称为不能整除),因此除法不遵循上述规则。
为了避免两个整型值相除和两个浮点型值相除导致不一样的结果,除法运算操作的永远是浮点数且产生浮点型值的结果:

1
2
> 3.0 / 2.0		--> 1.5
> 3 / 2 --> 1.5

Lua 5.3针对整数除法引入了一个称为floor除法的新算术运算符//。
顾名思义,floor除法会对得到的商向负无穷取整,从而保证结果是一个整数。
这样,floor除法就可以与其他算术运算一样遵循同样的规则:如果操作数都是整型值,那么结果就是整型值,否则就是浮点型值(其值是一个整数)。

1
2
3
4
5
6
7
8
9
> 3 // 2	--> 1
> 3.0 // 2 --> 1.0
> 6 // 2 --> 3
> 6.0 // 2.0 --> 3.0
> -9 // 2 --> -5
> 1.5 // 0.5 --> 3.0
```
## 3.7运算符优先级
Lua语言中的运算符优先级如下(优先级从高到低):

^

  • - not (一元运算符)

  • / // %
  • -
    .. (连接)
    << >> (按位移位)
    & (按位与)
    ~ (按位异或)
    | (按位或)
    < > <= >= ~= ==
    and
    or
    1
    在二元运算符中,除了幂运算和连接操作符是右结合的外,其他运算符都是左结合的。因此,以下各个表达式的左右两边等价:
    a+i<b/2+1 <–> (a + i) < ((b / 2) + 1)
    5+x^2*8 <–> 5 + ((x^2) * 8)
    a<y and y<=z <–> (a < y) and (y <= z)

-x^2 <–> -(x^2)
x^y^z <–> x^(y^z)

1
2
3
4
5
6
7
8
9
10
11
12
 当不能确定某些表达式的运算符优先级时,应该显式地用括号来指定所希望的运算次序。
# Chapter4 字符串
Lua语言中的字符串是一串字节组成的序列,Lua核心并不关心这些字节究竟以何种方式编码文本。在Lua语言中,字符使用8个比特位来存储(eight-bit clean)。
Lua语言中的字符串可以存储包括空字符在内的所有数值代码,这意味着我们可以在字符串中存储任意的二进制数据。

Lua语言中的字符串是不可变值(immutable value)。
我们不能像在C语言中那样直接改变某个字符串中的某个字符,但是我们可以通过创建一个新字符串的方式来达到修改的目的,例如:
```c
a = "one string"
b = string.gsub(a,"one","another") --改变字符串中的某些部分
print(a) --> one string
print(b) --> another string

像Lua语言中的其他对象(表、函数等)一样,Lua语言中的字符串也是自动内存管理的对象之一。
这意味着Lua语言会负责字符串的分配和释放,开发人员无须关注。

可以使用长度操作符(length operator)(#)获取字符串的长度:

1
2
3
a = "hello"
print(#a) --> 5
print(#"good bye") --> 8

该操作符返回字符串占用的字节数,在某些编码中,这个值可能与字符串中字符的个数不同。

应该注意,在Lua语言中,字符串是不可变量。字符串连接总是创建一个新字符串,而不会改变原来作为操作数的字符串:

1
2
3
> a = "Hello"
> a .. "World" --> Hello World
> a --> Hello

4.1字符串常量

我们可以使用一对双引号或单引号来声明字符串常量(literal string)。
使用双引号和单引号声明字符串是等价的。
它们两者唯一的区别在于,使用双引号声明的字符串中出现单引号时,单引号可以不用转义;使用单引号声明的字符串中出现双引号时,双引号可以不用转义。

4.2长字符串/多行字符串

有时字符串中可能有类似a=b[c[i]]这样的内容(注意其中的]]),或者,字符串中可能有被注释掉的代码。
为了应对这些情况,可以在两个左方括号之间加上任意数量的等号,如[===[。这样,字符串常量只有在遇到了包含相同数量等号的两个右方括号时才会结束(就前例而言,即]===])。
Lua语言的语法扫描器会忽略所含等号数量不相同的方括号。通过选择恰当数量的等号,就可以在无须修改原字符串的情况下声明任意的字符串常量了。

对注释而言,这种机制也同样有效。例如,我们可以使用–[=[和]=]来进行长注释,从而降低了对内部已经包含注释的代码进行注释的难度。

4.3强制类型转换

Lua语言在运行时提供了数值与字符串之间的自动转换(conversion)。针对字符串的所有算术操作会尝试将字符串转换为数值。
Lua语言不仅仅在算术操作时进行这种强制类型转换(coercion),还会在任何需要数值的情况下进行,例如函数math.sin的参数。

相反,当Lua语言发现在需要字符串的地方出现了数值时,它就会把数值转换为字符串:

1
print(10 .. 20)		-->		1020

很多人认为自动强制类型转换算不上是Lua语言中的一项好设计。作为原则之一,建议最好不要完全寄希望于自动强制类型转换。虽然在某些场景下这种机制很便利,但同时也给语言和使用这种机制的程序带来了复杂性。

作为这种“二类状态(second-class status)”的表现之一,Lua 5.3没有实现强制类型转换与整型的集成,而是采用了另一种更简单和快速的实现方式:算术运算的规则就是只有在两个操作数都是整型值时结果才是整型。
因此,由于字符串不是整型值,所以任何有字符串参与的算术运算都会被当作浮点运算处理:

1
>	"10" + 1		-->		11.0

如果需要显式地将一个字符串转换成数值,那么可以使用函数tonumber。
当这个字符串的内容不能表示为有效数字时该函数返回nil;否则,该函数就按照Lua语法扫描器的规则返回对应的整型值或浮点类型值。

与算术操作不同,比较操作符不会对操作数进行强制类型转换。请注意,”0”和0是不同的。
此外,2<15明显为真,但”2”<”15”却为假(字母顺序)。
为了避免出现不一致的结果,当比较操作符中混用了字符串和数值(比如2<”15”)时,Lua语言会抛出异常。

4.4字符串标准库

字符串标准库中的一些函数非常简单:
函数string.len(s)返回字符串s的长度,等价于#s。
函数string.rep(s,n)返回将字符串s重复n次的结果。可以通过调用string.rep(”a”,2^20)创建一个1MB大小的字符串(例如用于测试)。
函数string.reverse用于字符串翻转。
函数string.lower(s)返回一份s的副本,其中所有的大写字母都被转换成小写字母,而其他字符则保持不变。
函数string.upper与之相反,该函数会将小写字母转换成大写字母。

1
2
3
4
5
6
7
8
9
> string.rep("abc",3)		--> abcabcabc
> string.reverse("A Long Line!") --> !eniL gnoL A
> string.lower("A Long Line!") --> a long line!
> string.upper("A Long Line!") --> A LONG LINE!
```

函数string.sub(s,i,j)从字符串s中提取第i个到第j个字符(包括第i个和第j个字符,字符串的第一个字符索引为1)。
该函数也支持负数索引,负数索引从字符串的结尾开始计数:索引-1代表字符串的最后一个字符,索引-2代表倒数第二个字符,依此类推。
这样,对字符串s调用函数string.sub(s,1,j)得到的是字符串s中长度为j的前缀,调用string.sub(s,j,-1)得到的是字符串s中从第j个字符开始的后缀,调用string.sub(s,2,-2)返回的是去掉字符串s中第一个和最后一个字符后的结果:

s = “[in brackets]”
string.sub(s,2,-2) –> in brackets
string.sub(s,1,1) –> [
string.sub(s,-1,-1) –> ]

1
2
3
请注意,Lua语言中的字符串是不可变的。和Lua语言中的所有其他函数一样,函数string.sub不会改变原有字符串的值,它只会返回一个新字符串。
一种常见的误解是以为string.sub(s,2,-2)返回的是修改后的s。
如果需要修改原字符串,那么必须把新的值赋值给它:

s = string.sub(s,2,-2)

1
2
3
4
5
6

函数string.char和string.byte用于转换字符及其内部数值表示。
函数string.char接收零个或多个整数作为参数,然后将每个整数转换成对应的字符,最后返回由这些字符连接而成的字符串。
函数string.byte(s,i)返回字符串s中第i个字符的内部数值表示,该函数的第二个参数是可选的。
调用string.byte(s)返回字符串s中第一个字符(如果字符串只由一个字符组成,那么就返回这个字符)的内部数值表示。
在下例中,假定字符是用ASCII表示的:

print(string.char(97)) –> abc
i = 99;print(string.char(i,i+1,i+2)) –> cde
print(string.byte(“abc”)) –> 97
print(string.byte(“abc”,2)) –> 98
print(string.byte(“abc”,-1)) –> 99

1
2
在最后一行中,使用了负数索引来访问字符串的最后一个字符。
调用string.byte(s,i,j)返回索引i到j之间(包括i和j)的所有字符的数值表示:

print(string.byte(“abc”,1,2)) –> 97 98

1
2
3
4
 一种常见的写法是{string.byte(s,1,-1)},该表达式会创建一个由字符串s中的所有字符代码组成的表(由于Lua语言限制了栈大小,所以也限制了一个函数的返回值的最大个数,默认最大为一百万个。因此,这个技巧不能用于大小超过1MB的字符串)。

函数string.format是用于进行字符串格式化和将数值输出为字符串的强大工具,该函数会返回第一个参数(也就是所谓的格式化字符串( format string))的副本,其中的每一个指示符(directive)都会被替换为使用对应格式进行格式化后的对应参数。
格式化字符串中的指示符与C语言中函数printf的规则类似,一个指示符由一个百分号和一个代表格式化方式的字母组成:d代表一个十进制整数、x代表一个十六进制整数、f代表一个浮点数、s代表字符串,等等。

string.format(“x = %d y = %d”,10,20) –> x = 10 y = 20
string.format(“x = %x”,200) –> x = c8
string.format(“x = 0x%X”,200) –> x = 0xC8
string.format(“x = %f”,200) –> x = 200.000000
tag,title = “h1”,”a title”
string.format(“<%s>%s</%s>”,tag,title,tag) –>

a title

1
2
3
在百分号和字母之间可以包含用于控制格式细节的其他选项。

例如,可以指定一个浮点数中小数点的位数:

print(string.format(“pi = %.4f”,math.pi)) –> pi = 3.1416
d = 5;m = 11;y = 1990
print(string.format(“%02d/%02d/%04d”,d,m,y)) –> 05/11/1990

1
2
3
4
5
6
7
8
在上例中,%.4f表示小数点后保留4位小数;%02d表示一个十进制数至少由两个数字组成,不足两个数字的用0补齐,而%2d则表示用空格来补齐。
关于这些指示符的完整描述可以参阅C语言printf函数的相关文档,因为Lua语言是通过调用C语言标准库来完成实际工作的。

可以使用冒号操作符像调用字符串的一个方法那样调用字符串标准库中的所有函数。
例如,string.sub(s,i,j)可以重写为s:sub(i,j),string.upper(s)可以重写为s:upper()。

字符串标准库还包括了几个基于模式匹配的函数。
函数string.find用于在指定的字符串中进行模式搜索:

string.find(“hello world”,”wor”) –> 7 9
string.find(“hello world”,”war”) –> nil

1
2
3
如果该函数在指定的字符串中找到了匹配的模式,则返回模式的开始和结束位置,否则返回nil。

函数string.gsub(Global SUBstitution)则把所有匹配的模式用另一个字符串替换:

string.gsub(“hello world”,”l”,”.”) –> he..o wor.d 3
string.gsub(“hello world”,”ll”,”..”) –> he..o world 1
string.gsub(“hello world”,”a”,”.”) –> hello world 0

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
该函数还会在第二个返回值中返回发生替换的次数。   
# Chapter5 表
表(Table)是Lua语言中最主要(事实上也是唯一的)和强大的数据结构。使用表,Lua语言可以以一种简单、统一且高效的方式表示数组、集合、记录和其他很多数据结构。Lua语言也使用表来表示包(package)和其他对象。
当调用函数math.sin时,我们可能认为是“调用了math库中函数sin”;而对于Lua语言来说,其实际含义是“以字符串"sin"为键检索表math”。

Lua语言中的表本质上是一种辅助数组(associative array),这种数组不仅可以使用数值作为索引,也可以使用字符串或其他任意类型的值作为索引(nil除外)。

Lua语言中的表要么是值要么是变量,它们都是对象( object)。可以认为,表是一种动态分配的对象,程序只能操作指向表的引用(或指针)。
除此以外,Lua语言不会进行隐藏的拷贝(hidden copies)或创建新的表 。(此处所谓的隐藏的拷贝是指深拷贝,即拷贝的是对象的引用而非整个对象本身。)

表永远是匿名的,表本身和保存表的变量之间没有固定的关系。

对于一个表而言,当程序中不再有指向它的引用时,垃圾收集器会最终删除这个表并重用其占用的内存。
## 5.1表索引
同一个表中存储的值可以具有不同的类型索引 ,并可以按需增长以容纳新的元素。

a = {} –空的表
– 创建1000个新元素
for i=1,1000 do a[i] = i*2 end
a[9] –> 18
a[“x”] = 10
a[“x”] –> 10
a[“y”] –> nil

1
2
3
请注意上述代码的最后一行:如同全局变量一样,未经初始化的表元素为nil,将nil赋值给表元素可以将其删除。这并非巧合,因为Lua语言实际上就是使用表来存储全局变量的(详见第22章)。

当把表当作结构体使用时,可以把索引当作成员名称使用(a.name等价于a["name"])。因此,可以使用这种更加易读的方式改写前述示例的最后几行:

a = {} –空的表
a.x = 10 –等价于a[“x”] = 10、
a.x –> 10 –等价于a[“x”]
a.y –> nil –等价于a[“y”]

1
2
3
4
5
对Lua语言而言,这两种形式是等价且可以自由混用的;不过,对于阅读程序的人而言,这两种形式可能代表了不同的意图。
形如a.name的点分形式清晰地说明了表是被当作结构体使用的,此时表实际上是由固定的、预先定义的键组成的集合;而形如a["name"]的字符串索引形式则说明了表可以使用任意字符串作为键,并且出于某种原因我们操作的是指定的键。

初学者常常会混淆a.x和a[x]。
实际上,a.x代表的是a["x"],即由字符串"x"索引的表;而a[x]则是指由变量x对应的值索引的表,例如:

a = {}
x = “y”
a[x] = 10 – 把10放在字段”y”中
a[x] –> 10 –字段”y”的值
a.x –> nil –字段”x”的值(未定义)
a.y –> 10 –字段”y”的值

1
2
3
4
5
6
7

由于可以使用任意类型索引表,所以在索引表时会遇到相等性比较方面的微妙问题。
虽然确实都能用数字0和字符串"0"对同一个表进行索引,但这两个索引的值及其所对应的元素是不同的。
同样,字符串"+1"、"01"和"1"指向的也是不同的元素。
当不能确定表索引的真实数据类型时,可以使用显式的类型转换。

整型和浮点型类型的表索引则不存在上述问题。由于2和2.0的值相等,所以当它们被当作表索引使用时指向的是同一个表元素:

a = {}
a[2.0] = 10
a[2.1] = 20
a[2] –> 10
a[2.1] –> 20

1
2
3
4
5
6
7
更准确地说,当被用作表索引时,任何能够被转换为整型的浮点数都会被转换成整型数。
例如,当执行表达式a[2.0]=10时,键2.0会被转换为2。相反,不能被转换为整型数的浮点数则不会发生上述的类型转换。
## 5.2表构造器
表构造器(Table Constructor)是用来创建和初始化表的表达式,也是Lua语言中独有的也是最有用、最灵活的机制之一。

正如我们此前已经提到的,最简单的构造器是空构造器{}。
表构造器也可以被用来初始化列表,例如,下例中使用字符串"Sunday"初始化了days[1](构造器第一个元素的索引是1而不是0)、使用字符串"Monday"初始化了days[2],依此类推:

days = {“Sunday”,”Monday”,”Tuesday”,”Wednesday”,”Thursday”,”Friday”,”Saturday”}

print(days[4]) –> Wednesday

1
2

Lua语言还提供了一种初始化记录式(record-like)表的特殊语法:

a = {x = 10 , y = 20}

1
上述代码等价于:

a = {}; a.x = 10; a.y = 20

1
2
3
4
5
不过,在第一种写法中,由于能够提前判断表的大小,所以运行速度更快。
无论使用哪种方式创建表,都可以随时增加或删除表元素。

不过,正如此前所提到的,使用合适的构造器来创建表会更加高效和易读。
在同一个构造器中,可以混用记录式(record-style)和列表式(list-style)写法:

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
上述的示例也同时展示了如何创建嵌套表(和构造器)以表达更加复杂的数据结构。
每一个元素polyline[i]都是代表一条记录的表:

print(polyline[2].x) –> -10
print(polyline[4].y) –> 1

1
2
3
4

不过,这两种构造器都有各自的局限。
例如,使用这两种构造器时,不能使用负数索引初始化表元素,也不能使用不符合规范的标识符作为索引。
对于这类需求,可以使用另一种更加通用的构造器,即通过方括号括起来的表达式显式地指定每一个索引:

opnames = {[“+”] = “add”,[“-“] = “sub”,
[“*”] = “mul”,[“/“] = “div”}

i = 20;s = “-“
a = {[i+0] = s,[i+1] = s..s,[i+2] = s..s..s}

print(opnames[s]) –> sub
print(a[22]) –> —

1
2
3

这种构造器虽然冗长,但却非常灵活,不管是记录式构造器还是列表式构造器均是其特殊形式。
例如,下面的几种表达式就相互等价:

{ x = 0 , y = 0 } <–> { [“x”] = 0 , [“y”] = 0 }
{ “r”,”g”,”b” } <–> {[1] = “r”,[2] = “g”,[3] = “b”}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
## 5.3数组、列表和序列
如果想表示常见的数组(array)或列表(list),那么只需要使用整型作为索引的表即可。同时,也不需要预先声明表的大小,只需要直接初始化我们需要的元素即可。

当操作列表时,往往必须事先获取列表的长度。列表的长度可以存放在常量中,也可以存放在其他变量或数据结构中。
通常,我们把列表的长度保存在表中某个非数值类型的字段中(由于历史原因,这个键通常是"n")。当然,列表的长度经常也是隐式的。
请注意,由于未初始化的元素均为nil,所以可以利用nil值来标记列表的结束。
例如,当向一个列表中写入了10行数据后,由于该列表的数值类型的索引为1,2,...,10,所以可以很容易地知道列表的长度就是10。
这种技巧只有在列表中不存在空洞(hole)时(即所有元素均不为nil)才有效,此时我们把这种所有元素都不为nil的数组称为序列(sequence)。

对于中间存在空洞(nil值)的列表而言,序列长度操作符是不可靠的,它只能用于序列(所有元素均不为nil的列表)。
更准确地说,序列(sequence)是由指定的 n 个正数数值类型的键所组成集合{1,...,n}形成的表(请注意值为nil的键实际不在表中)。
特别地,不包含数值类型键的表就是长度为零的序列。

## 5.4 遍历表
我们可以使用pairs迭代器遍历表中的键值对:

t = {10,print,x = 12,k = “hi”}
for k,v in pairs(t) do
print(k,v)
end

—> 1 10
—> k hi
—> 2 function: 0x420610
—> x 12

1
2
3
4
受限于表在Lua语言中的底层实现机制,遍历过程中元素的出现顺序可能是随机的,相同的程序在每次运行时也可能产生不同的顺序。
唯一可以确定的是,在遍历的过程中每个元素会且只会出现一次。

对于列表而言,可以使用ipairs迭代器:

t = {10,print,12,”hi”}
for k,v in ipairs(t) do
print(k,v)
end

—> 1 10
—> 2 function: 0x420610
—> 3 12
—> 4 hi

1
2
3
此时,Lua会确保遍历是按照顺序进行的。

另一种遍历序列的方法是使用数值型for循环:

t = {10,print,12,”hi”}
for k=1,#t in do
print(k,t[k])
end

—> 1 10
—> 2 function: 0x420610
—> 3 12
—> 4 hi

1
2
3
4
## 5.5安全访问
考虑如下的情景:我们想确认在指定的库中是否存在某个函数。如果我们确定这个库确实存在,那么可以直接使用if lib.foo then...;否则,就得使用形如if lib and lib.foo then...的表达式。

当表的嵌套深度变得比较深时,这种写法就会很容易出错,例如:

zip = company and company.director and
company.director.address and
company.director.address.zipcode

1
2
3
4
5
这种写法不仅冗长而且低效,该写法在一次成功的访问中对表进行了6次访问而非3次访问。


对于这种情景,诸如C#的一些编程语言提供了一种安全访问操作符(safe navigation operator)。
在C#中,这种安全访问操作符被记为“?.”。例如,对于表达式a?.b,当a为nil时,其结果是nil而不会产生异常。使用这种操作符,可以将上例改写为:

zip = company?.director?.address?.zipcode

1
2
3
4
5
6
7
8
如果上述的成员访问过程中出现nil,安全访问操作符会正确地处理nil并最终返回nil。

Lua语言并没有提供安全访问操作符,并且认为也不应该提供这种操作符。
一方面,Lua语言在设计上力求简单;另一方面,这种操作符也是非常有争议的,很多人就无理由地认为该操作符容易导致无意的编程错误。
不过,我们可以使用其他语句在Lua语言中模拟安全访问操作符。

对于表达式a or {},当a为nil时其结果是一个空表。因此,对于表达式(a or{}).b,当a为nil时其结果也同样是nil。
这样,我们就可以将之前的例子重写为:

zip = (((company or {}).director or {}).address or {}).zipcode

1
再进一步,我们还可以写得更短和更高效:

E = {} – 可以在其他类似表达式中复用

zip = (((company or E).director or E).address or E).zipcode

1
2
3
4
5
6
7
8
9
10
11
12
13
14

确实,上述的语法比安全访问操作符更加复杂。
不过尽管如此,表中的每一个字段名都只被使用了一次,从而保证了尽可能少地对表进行访问(本例中对表仅有3次访问);同时,还避免了向语言中引入新的操作符。
就我个人看来,这已经是一种足够好的替代方案了。
## 5.6表标准库
表标准库提供了操作列表和序列的一些常用函数。

函数table.insert向序列的指定位置插入一个元素,其他元素依次后移。
例如,对于列表t={10,20,30},在调用table.insert(t,1,15)后它会变成{15,10,20,30},另一种特殊但常见的情况是调用insert时不指定位置,此时该函数会在序列的最后插入指定的元素,而不会移动任何元素。

函数table.remove删除并返回序列指定位置的元素,然后将其后的元素向前移动填充删除元素后造成的空洞。如果在调用该函数时不指定位置,该函数会删除序列的最后一个元素。

Lua 5.3对于移动表中的元素引入了一个更通用的函数table.move(a,f,e,t),调用该函数可以将表a中从索引f到e的元素(包含索引f和索引e对应的元素本身)移动到位置t上。
例如,如下代码可以在列表a的开头插入一个元素:

table.move(a,1,#a,2);
a[1] = newElement

1
如下的代码可以删除第一个元素:

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

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
应该注意,在计算机领域,移动(move)实际上是将一个值从一个地方拷贝(copy)到另一个地方。因此,像上面的例子一样,我们必须在移动后显式地把最后一个元素删除。

函数table.move还支持使用一个表作为可选的参数。
当带有可选的表作为参数时,该函数将第一个表中的元素移动到第二个表中。
例如,table.move(a,1,#a,1,{})返回列表a的一个克隆(clone)(通过将列表a中的所有元素拷贝到新列表中),table.move(a,1,#a,#b+1,b)将列表a中的所有元素复制到列表b的末尾 。
# Chapter6 函数
在Lua语言中,函数(Function)是对语句和表达式进行抽象的主要方式。函数既可以用于完成某种特定任务(有时在其他语言中也称为过程(procedure)或子例程( subroutine)),也可以只是进行一些计算然后返回计算结果。
在前一种情况下,我们将一句函数调用视为一条语句;而在后一种情况下,我们则将函数调用视为表达式。

一个Lua程序既可以调用Lua语言编写的函数,也可以调用C语言(或者宿主程序使用的其他任意语言)编写的函数。
一般来说,我们选择使用C语言编写的函数来实现对性能要求更高,或不容易直接通过Lua语言进行操作的操作系统机制等。
例如,Lua语言标准库中所有的函数就都是使用C语言编写的。
不过,无论一个函数是用Lua语言编写的还是用C语言编写的,在调用它们时都没有任何区别。

在这种语法中,一个函数定义具有一个函数名(name,本例中的add)、一个参数(parameter)组成的列表和由一组语句组成的函数体(body)。
参数的行为与局部变量的行为完全一致,相当于一个用函数调用时传入的值进行初始化的局部变量。

调用函数时使用的参数个数可以与定义函数时使用的参数个数不一致。Lua语言会通过抛弃多余参数和将不足的参数设为nil的方式来调整参数的个数。
## 6.1多返回值
Lua语言中一种与众不同但又非常有用的特性是允许一个函数返回多个结果(Multiple Results)。

Lua语言根据函数的被调用情况调整返回值的数量。
当函数被作为一条单独语句调用时,其所有返回值都会被丢弃;当函数被作为表达式(例如,加法的操作数)调用时,将只保留函数的第一个返回值。
只有当函数调用是一系列表达式中的最后一个表达式(或是唯一一个表达式)时,其所有的返回值才能被获取到。
这里所谓的“一系列表达式”在Lua中表现为4种情况:多重赋值、函数调用时传入的实参列表、表构造器和return语句。

为了分别展示这几种情况,接下来举几个例子:

function foo0() end – 不返回结果
function foo1() return “a” end – 返回1个结果
function foo2() return “a”,”b” end – 返回2个结果

1
2

在多重赋值中,如果一个函数调用是一系列表达式中的最后(或者是唯一)一个表达式,则该函数调用将产生尽可能多的返回值以匹配待赋值变量:

x,y = foo2() – x = “a”,y = “b”
x = foo2() – x = “a”,”b”被丢弃
x,y,z = 10,foo2() – x = 10,y = “a”,z = “b”

1
2

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

x,y = foo0() – x = nil,y = nil
x,y = foo1() – x = “a”,y = nil
x,y,z = 10,foo2() – x = 10,y = “a”,z = “b”

1
2

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

x,y = foo2(),20 – x = “a”,y = 20 (“b”被丢弃)
x,y = foo0(),20,30 – x = nil,y = 20 (30被丢弃)

1
2
3

当一个函数调用是另一个函数调用的最后一个(或者是唯一)实参时,第一个函数的所有返回值都会被作为实参传给第二个函数。
我们已经见到过很多这样的代码结构,例如函数print。由于函数print能够接收可变数量的参数,所以print(g())会打印出g返回的所有结果。

print(foo0()) –> (没有结果)
print(foo1()) –> a
print(foo2()) –> a b
print(foo2(),1) –> a 1
print(foo2()..”x”) –> ax

1
2
3
4
5
当在表达式中调用foo2时,Lua语言会把其返回值的个数调整为1。因此,在上例的最后一行,只有第一个返回值"a"参与了字符串连接操作。

当我们调用f(g())时,如果f的参数是固定的,那么Lua语言会把g返回值的个数调整成与f的参数个数一致。这并非巧合,实际上这正是多重赋值的逻辑。

表构造器会完整地接收函数调用的所有返回值,而不会调整返回值的个数:

t = {foo0()} – t = {}(一个空表)
t = {foo1()} – t = {“a”}
t = {foo2()} – t = {“a”,”b”}

1
不过,这种行为只有当函数调用是表达式列表中的最后一个时才有效,在其他位置上的函数调用总是只返回一个结果:

t = {foo0(),foo2(),4} – t[1] = nil,t[2] = “a”,t[3] = 4

1
2

最后,形如return f()的语句会返回f返回的所有结果:

function foo(i)
if i == 0 then return foo0()
elseif i == 1 then return foo1()
elseif i == 2 then return foo2()
end
end

print(foo(1)) –> a
print(foo(2)) –> a b
print(foo(0)) –> (无结果)
print(foo(3)) –> (无结果)

1
2

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

print((foo(0))) –> nil
print((foo(1))) –> a
print((foo(2))) –> a

1
2
3
4
## 6.2可变长参数函数
Lua语言中的函数可以是可变长参数函数(variadic),即可以支持数量可变的参数。

下面是一个简单的示例,该函数返回所有参数的总和:

function add (…)
local s = 0
for _,v in ipairs {…} do
s = s + v
end

return s

end

print(add(3,4,10,25,12)) –> 54

1
2
3
4
5
6
7
8
9
10
11
12
13
参数列表中的三个点(...)表示该函数的参数是可变长的。
当这个函数被调用时,Lua内部会把它的所有参数收集起来,我们把这些被收集起来的参数称为函数的额外参数(extra argument)。
当函数要访问这些参数时仍需用到三个点,但不同的是此时这三个点是作为一个表达式来使用的。
在上例中,表达式{...}的结果是一个由所有可变长参数组成的列表,该函数会遍历该列表来累加其中的元素。

我们将三个点组成的表达式称为可变长参数表达式(vararg expression),其行为类似于一个具有多个返回值的函数,返回的是当前函数的所有可变长参数。

具有可变长参数的函数也可以具有任意数量的固定参数,但固定参数必须放在变长参数之前。Lua语言会先将前面的参数赋给固定参数,然后将剩余的参数(如果有)作为可变长参数。

要遍历可变长参数,函数可以使用表达式{...}将可变长参数放在一个表中,就像add示例中所做的那样。
不过,在某些罕见的情况下,如果可变长参数中包含无效的nil,那么{...}获得的表可能不再是一个有效的序列。此时,就没有办法在表中判断原始参数究竟是不是以nil结尾的。
对于这种情况,Lua语言提供了函数table.pack。 该函数像表达式{...}一样保存所有的参数,然后将其放在一个表中返回,但是这个表还有一个保存了参数个数的额外字段"n"。
例如,下面的函数使用了函数table.pack来检测参数中是否有nil:

function nonils (…)
local arg = table.pack(…)
for i = 1,arg.n do
if arg[i] == nil then
return false
end
end

return true

end

print(nonils(2,3,nil)) –> false
print(nonils(2,3)) –> true
print(nonils()) –> true
print(nonils(nil)) –> false

1
2
3
4

另一种遍历函数的可变长参数的方法是使用函数select。
函数select总是具有一个固定的参数 selector,以及数量可变的参数。
如果selector是数值n,那么函数select则返回第n个参数后的所有参数;否则,selector应该是字符串"#",以便函数select返回额外参数的总数。

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
通常,我们在需要把返回值个数调整为1的地方使用函数select,因此可以把select(n,...)认为是返回第n个额外参数的表达式。

来看一个使用函数select的典型示例,下面是使用该函数的add函数:

function add (…)
local s = 0
for i = 1,select(“#”,……) do
s = s + select(i,…)
end

return s

end

1
2
3
4
5
6
7
8
9
10
11
12
对于参数较少的情况,第二个版本的add更快,因为该版本避免了每次调用时创建一个新表。
不过,对于参数较多的情况,多次带有很多参数调用函数select会超过创建表的开销,因此第一个版本会更好(特别地,由于迭代的次数和每次迭代时传入参数的个数会随着参数的个数增长,因此第二个版本的时间开销是二次代价(quadratic cost)的)。
## 6.3函数table.unpack
多重返回值还涉及一个特殊的函数table.unpack。该函数的参数是一个数组,返回值为数组内的所有元素。

顾名思义,函数table.unpack与函数table.pack的功能相反。
pack把参数列表转换成Lua语言中一个真实的列表(一个表),而unpack则把Lua语言中的真实的列表(一个表)转换成一组返回值,进而可以作为另一个函数的参数被使用。

unpack函数的重要用途之一体现在泛型调用(generic call)机制中。
泛型调用机制允许我们动态地调用具有任意参数的任意函数。例如,在ISO C中,我们无法编写泛型调用的代码,只能声明可变长参数的函数(使用stdarg.h)或使用函数指针来调用不同的函数。
但是,我们仍然不能调用具有可变数量参数的函数,因为C语言中的每一个函数调用的实参个数是固定的,并且每个实参的类型也是固定的。
而在Lua语言中,却可以做到这一点。如果我们想通过数组a传入可变的参数来调用函数f,那么可以写成:

f(table.unpack(a))

1
2
unpack会返回a中所有的元素,而这些元素又被用作f的参数。
例如,考虑如下的代码:

print(string.find(“hello”,”ll”))

1
可以使用如下的代码动态地构造一个等价的调用:

f = string.find
a = {“hello”,”ll”}

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

1
2

通常,函数table.unpack使用长度操作符获取返回值的个数,因而该函数只能用于序列。不过,如果有需要,也可以显式地限制返回元素的范围:

print(table.unpack({“Sun”,”Mon”,”Tue”,”Wed”},2,3))
–> Mon Tue

1
2

虽然预定义的函数unpack是用C语言编写的,但是也可以利用递归在Lua语言中实现:

function unpack(t,i,n)
i = i or 1
n = n or #t
if i<=n then
return t[i],unpack(t,i+1,n)
end
end

1
2
3
4
5
在第一次调用该函数时,只传入一个参数,此时i为1,n为序列长度;然后,函数返回t[1]及unpack(t,2,n)返回的所有结果,而unpack(t,2,n)又会返回t[2]及unpack(t,3,n)返回的所有结果,依此类推,直到处理完n个元素为止。
## 6.4正确的尾调用
Lua语言中有关函数的另一个有趣的特性是,Lua语言是支持尾调用消除(tail-call elimination)的。这意味着Lua语言可以正确地(properly)尾递归(tail recursive ),虽然尾调用消除的概念并没有直接涉及递归。

尾调用(tail call)是被当作函数调用使用的跳转 。当一个函数的最后一个动作是调用另一个函数而没有再进行其他工作时,就形成了尾调用。例如,下列代码中对函数g的调用就是尾调用:

function f(x)
x = x + 1
return g(x)
end

1
2
3
4
5
当函数f调用完函数g之后,f不再需要进行其他的工作。这样,当被调用的函数执行结束后,程序就不再需要返回最初的调用者。
因此,在尾调用之后,程序也就不需要在调用栈中保存有关调用函数的任何信息。当g返回时,程序的执行路径会直接返回到调用f的位置。
在一些语言的实现中,例如Lua语言解释器,就利用了这个特点,使得在进行尾调用时不使用任何额外的栈空间。我们就将这种实现称为尾调用消除(tail-call elimination)。

由于尾调用不会使用栈空间,所以一个程序中能够嵌套的尾调用的数量是无限的。例如,下列函数支持任意的数字作为参数:

function foo(n)
if n > 0 then
return foo(n - 1)
end
end

1
2
3
4
该函数永远不会发生栈溢出。

关于尾调用消除的一个重点就是如何判断一个调用是尾调用。
很多函数调用之所以不是尾调用,是由于这些函数在调用之后还进行了其他工作。例如,下例中调用g就不是尾调用:

function f(x)
g(x)
end

1
2
这个示例的问题在于,当调用完g后,f在返回前还不得不丢弃g返回的所有结果。
类似的,以下的所有调用也都不符合尾调用的定义:

return g(x) + 1 – 必须进行加法
return x or g(x) – 必须把返回值限制为1个
return (g(x)) – 必须把返回值限制为1个

1
2
3
4

在Lua语言中,只有形如return func(args)的调用才是尾调用。
不过,由于Lua语言会在调用前对func及其参数求值,所以func及其参数都可以是复杂的表达式。
例如,下面的例子就是尾调用:

return x[i].foo(x[j] + a*b,i+j)

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
# Chapter7		输入输出
由于Lua语言强调可移植性和嵌入性,所以Lua语言本身并没有提供太多与外部交互的机制。
在真实的Lua程序中,从图形、数据库到网络的访问等大多数I/O操作,要么由宿主程序实现,要么通过不包括在发行版中的外部库实现。
单就Lua语言而言,只提供了ISO C语言标准支持的功能,即基本的文件操作等。
## 7.1简单I/O模型
对于文件操作来说,I/O库提供了两种不同的模型。
简单模型虚拟了一个当前输入流(current input stream)和一个当前输出流(current output stream),其I/O操作是通过这些流实现的。
I/O库把当前输入流初始化为进程的标准输入(C语言中的stdin),将当前输出流初始化为进程的标准输出(C语言中的stdout)。
因此,当执行类似于io.read()这样的语句时,就可以从标准输入中读取一行。

函数io.input和函数io.output可以用于改变当前的输入输出流。
调用io.input(filename)会以只读模式打开指定文件,并将文件设置为当前输入流。之后,所有的输入都将来自该文件,除非再次调用io.input。
对于输出而言,函数io.output的逻辑与之类似。
如果出现错误,这两个函数都会抛出异常。如果想直接处理这些异常,则必须使用完整I/O模型。

由于函数write比函数read简单,我们首先来看函数write。
函数io.write可以读取任意数量的字符串(或者数字)并将其写入当前输出流。
由于调用该函数时可以使用多个参数,因此应该避免使用io.write(a..b..c),应该调用io.write(a,b,c),后者可以用更少的资源达到同样的效果,并且可以避免更多的连接动作。

作为原则,应该只在“用后即弃”的代码或调试代码中使用函数print;当需要完全控制输出时,应该使用函数io.write。
与函数print不同,函数io.write不会在最终的输出结果中添加诸如制表符或换行符这样的额外内容。
此外,函数io.write允许对输出进行重定向,而函数print只能使用标准输出。
最后,函数print可以自动为其参数调用tostring,这一点对于调试而言非常便利,但这也容易导致一些诡异的Bug。

函数io.write在将数值转换为字符串时遵循一般的转换规则;如果想要完全地控制这种转换,则应该使用函数string.format。

函数io.read可以从当前输入流中读取字符串,其参数决定了要读取的数据:

“a” 读取整个文件
“l” 读取下一行(丢弃换行符)
“L” 读取下一行(保留换行符)
“n” 读取一个数值
num 以字符串读取num个字符

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20

调用io.read("a")可从当前位置开始读取当前输入文件的全部内容。如果当前位置处于文件的末尾或文件为空,那么该函数返回一个空字符串。

因为Lua语言可以高效地处理长字符串,所以在Lua语言中编写过滤器(filter)的一种简单技巧就是将整个文件读取到一个字符串中,然后对字符串进行处理,最后输出结果。

调用io.read("l")会返回当前输入流的下一行,不包括换行符在内;调用io.read("L")与之类似,但会保留换行符(如果文件中存在)。
当到达文件末尾时,由于已经没有内容可以返回,该函数会返回nil。
选项"l"是函数read的默认参数。
我通常只在逐行处理数据的算法中使用该参数,其他情况则更倾向于使用选项"a"一次性地读取整个文件,或者像后续介绍的按块(block)读取。

调用io.read("n")会从当前输入流中读取一个数值,这也是函数read返回值为数值(整型或者浮点型,与Lua语法扫描器的规则一致)而非字符串的唯一情况。
如果在跳过了空格后,函数io.read仍然不能从当前位置读取到数值(由于错误的格式问题或到了文件末尾),则返回nil。

除了上述这些基本的读取模式外,在调用函数read时还可以用一个数字n作为其参数:在这种情况下,函数read会从输入流中读取n个字符。
如果无法读取到任何字符(处于文件末尾)则返回nil;否则,则返回一个由流中最多n个字符组成的字符串。

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

调用函数read时可以指定多个选项,函数会根据每个参数返回相应的结果。
假设有一个每行由3个数字组成的文件:

6.0 -3.23 15e12
4.3 234 1000001

1
如果想打印每一行的最大值,那么可以通过调用函数read来一次性地同时读取每行中的3个数字:

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
## 7.2完整I/O模型 
简单I/O模型对简单的需求而言还算适用,但对于诸如同时读写多个文件等更高级的文件操作来说就不够了。对于这些文件操作,我们需要用到完整I/O模型。

可以使用函数io.open来打开一个文件,该函数仿造了C语言中的函数fopen。
这个函数有两个参数,一个参数是待打开文件的文件名,另一个参数是一个模式(mode)字符串。
模式字符串包括表示只读的r、表示只写的w(也可以用来删除文件中原有的内容)、表示追加的a,以及另外一个可选的表示打开二进制文件的b。
函数io.open返回对应文件的流。
当发生错误时,该函数会在返回nil的同时返回一条错误信息及一个系统相关的错误码。

检查错误的一种典型方法是使用函数assert。
如果函数io.open执行失败,错误信息会作为函数assert的第二个参数被传入,之后函数assert会将错误信息展示出来。

在打开文件后,可以使用方法read和write从流中读取和向流中写入。它们与函数read和write类似,但需要使用冒号运算符将它们当作流对象的方法来调用。
例如,可以使用如下的代码打开一个文件并读取其中所有内容:

local f = assert(io.open(filename,”r”))
local t = f:read(“a”)
f:close()

1
2
3

I/O库提供了三个预定义的C语言流的句柄:io.stdin、io.stdout和io.stderr。
例如,可以使用如下的代码将信息直接写到标准错误流中:

io.stderr:write(message)

1
2
3
4

函数io.input和io.output允许混用完整I/O模型和简单I/O模型。
调用无参数的io.input()可以获得当前输入流,调用io.input(handle)可以设置当前输入流(类似的调用同样适用于函数io.output)。
例如,如果想要临时改变当前输入流,可以像这样:

local temp = io.input() – 保存当前输入流
io.input(“newInput”) – 打开一个新的当前输入流
– 对新的输入流进行某些操作
io.input():close() – 关闭当前流
io.input(temp) – 恢复此前的当前输入流

1
2
3
4
5
6
7
8
9
注意,io.read(args)实际上是io.input():read(args)的简写,即函数read是用在当前输入流上的。同样,io.write(args)是io.output():write(args)的简写。

除了函数io.read外,还可以用函数io.lines从流中读取内容。
正如之前的示例中展示的那样,函数io.lines返回一个可以从流中不断读取内容的迭代器。
给函数io.lines提供一个文件名,它就会以只读方式打开对应该文件的输入流,并在到达文件末尾后关闭该输入流。若调用时不带参数,函数io.lines就从当前输入流读取。
我们也可以把函数lines当作句柄的一个方法。

此外,从Lua 5.2开始,函数io.lines可以接收和函数io.read一样的参数。
例如,下面的代码会以在8KB为块迭代,将当前输入流中的内容复制到当前输出流中:

for block in io.input():lines(2^13) do
io.write(block)
end

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
## 7.3其他文件操作
函数io.tmpfile返回一个操作临时文件的句柄,该句柄是以读/写模式打开的。当程序运行结束后,该临时文件会被自动移除(删除)。

函数flush将所有缓冲数据写入文件。
与函数write一样,我们也可以把它当作io.flush()使用,以刷新当前输出流;或者把它当作方法f:flush()使用,以刷新流f。

函数setvbuf用于设置流的缓冲模式。
该函数的第一个参数是一个字符串:"no"表示无缓冲,"full"表示在缓冲区满时或者显式地刷新文件时才写入数据,"line"表示输出一直被缓冲直到遇到换行符或从一些特定文件(例如终端设备)中读取到了数据。
对于后两个选项,函数setvbuf支持可选的第二个参数,用于指定缓冲区大小。

在大多数系统中,标准错误流(io.stderr)是不被缓冲的,而标准输出流(io.stdout)按行缓冲。
因此,当向标准输出中写入了不完整的行(例如进度条)时,可能需要刷新这个输出流才能看到输出结果。

函数seek用来获取和设置文件的当前位置,常常使用f:seek(whence,offset)的形式来调用,其中参数whence是一个指定如何使用偏移的字符串。
当参数whence取值为"set"时,表示相对于文件开头的偏移;取值为"cur"时,表示相对于文件当前位置的偏移;取值为"end"时,表示相对于文件尾部的偏移。
不管whence的取值是什么,该函数都会以字节为单位,返回当前新位置在流中相对于文件开头的偏移。
whence的默认值是"cur",offset的默认值是0。
因此,调用函数file:seek()会返回当前的位置且不改变当前位置;调用函数file:seek("set")会将位置重置到文件开头并返回0;调用函数file:seek("end")会将当前位置重置到文件结尾并返回文件的大小。
下面的函数演示了如何在不修改当前位置的情况下获取文件大小:

function fsize(file)
local current = file:seek() – 保存当前位置
local size = file:seek(“end”) – 获取文件大小
file:seek(“set”,current) – 恢复当前位置

return size

end

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

此外,函数os.rename用于文件重命名,函数os.remove用于移除(删除)文件。需要注意的是,由于这两个函数处理的是真实文件而非流,所以它们位于os库而非io库中。

上述所有的函数在遇到错误时,均会返回nil外加一条错误信息和一个错误码。
## 7.4 其他系统调用
函数os.exit用于终止程序的执行。
该函数的第一个参数是可选的,表示该程序的返回状态,其值可以为一个数值(0表示执行成功)或者一个布尔值(true表示执行成功);
该函数的第二个参数也是可选的,当值为true时会关闭Lua状态并调用所有析构器释放所占用的所有内存(这种终止方式通常是非必要的,因为大多数操作系统会在进程退出时释放其占用的所有资源)。

函数os.getenv用于获取某个环境变量,该函数的输入参数是环境变量的名称,返回值为保存了该环境变量对应值的字符串。
对于未定义的环境变量,该函数返回nil。
### 7.4.1运行系统命令
函数os.execute用于运行系统命令,它等价于C语言中的函数system。
该函数的参数为表示待执行命令的字符串,返回值为命令运行结束后的状态。
其中,第一个返回值是一个布尔类型,当为true时表示程序成功运行完成;
第二个返回值是一个字符串,当为"exit"时表示程序正常运行结束,当为"signal"时表示因信号而中断;
第三个返回值是返回状态(若该程序正常终结)或者终结该程序的信号代码。

例如,在POSIX和Windows中都可以使用如下的函数创建新目录:

function createDir(dirname)
return os.execute(“mkdir”..dirname)
end

1
2
3
4
5

另一个非常有用的函数是io.popen。
同函数os.execute一样,该函数运行一条系统命令,但该函数还可以重定向命令的输入/输出,从而使得程序可以向命令中写入或从命令的输出中读取。

例如,下列代码使用当前目录中的所有内容构建了一个表:

– 对于POSIX系统而言,使用’ls’而非’dir’
local f = io.popen(“dir /B”,”r”)
local dir = {}
for entry in f:lines() do
dir[#dir + 1] = entry
end

1
2
3
4
5
6
7
8
9
10
其中,函数io.popen的第二个参数"r"表示从命令的执行结果中读取。由于该函数的默认行为就是这样,所以在上例中这个参数实际是可选的。

正如我们在上面的两个例子中看到的一样,函数os.execute和io.popen都是功能非常强大的函数,但它们也同样是非常依赖于操作系统的。
# Chapter8 补充知识
## 8.1 局部变量和代码块
Lua语言中的变量在默认情况下是全局变量,所有的局部变量在使用前必须声明。与全局变量不同,局部变量的生效范围仅限于声明它的代码块。
一个代码块(block)是一个控制结构的主体,或是一个函数的主体,或是一个代码段(即变量被声明时所在的文件或字符串)。

一旦输入了do,命令就只会在遇到匹配的end时才结束,这样Lua语言解释器就不会单独执行每一行的命令。
当需要更好地控制某些局部变量的生效范围时,do程序块也同样有用:

local x1,x2
do
local a2 = 2a
local d = (b^2 - 4
a*c)^(1/2)
x1 = (-b + d)/a2
x2 = (-b - d)/a2
end – ‘a2’和’d’的范围在此结束
print(x1,x2) – ‘x1’和’x2’仍在范围内

1
2
3
4
5
6
7
8
9
10

尽可能地使用局部变量是一种良好的编程风格。
首先,局部变量可以避免由于不必要的命名而造成全局变量的混乱;
其次,局部变量还能避免同一程序中不同代码部分中的命名冲突;
再次,访问局部变量比访问全局变量更快;
最后,局部变量会随着其作用域的结束而消失,从而使得垃圾收集器能够将其释放。

局部变量的声明可以包含初始值,其赋值规则与常见的多重赋值一样:多余的值被丢弃,多余的变量被赋值为nil。如果一个声明中没有赋初值,则变量会被初始化为nil。

Lua语言中有一种常见的用法:

local foo = foo

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
这段代码声明了一个局部变量foo,然后用全局变量foo对其赋初值(局部变量foo只有在声明之后才能被访问)。
这个用法在需要提高对foo的访问速度时很有用。当其他函数改变了全局变量foo的值,而代码段又需要保留foo的原始值时,这个用法也很有用,尤其是在进行运行时动态替换(monkey patching,猴子补丁)时。
即使其他代码把print动态替换成了其他函数,在local print=print语句之前的所有代码使用的还都是原先的print函数。
## 8.2控制结构
Lua语言提供了一组精简且常用的控制结构(control structure),包括用于条件执行的if以及用于循环的while、repeat和for。
所有的控制结构语法上都有一个显式的终结符:end用于终结if、for及while结构,until用于终结repeat结构。

控制结构的条件表达式(condition expression)的结果可以是任何值。请记住,Lua语言将所有不是false和nil的值当作真(特别地,Lua语言将0和空字符串也当作真)。
### 8.2.1 if then else
if语句先测试其条件,并根据条件是否满足执行相应的then部分或else部分。else部分是可选的。
如果要编写嵌套的if语句,可以使用elseif。它类似于在else后面紧跟一个if,但可以避免重复使用end。
### 8.2.2 while
顾名思义,当条件为真时while循环会重复执行其循环体。
Lua语言先测试while语句的条件,若条件为假则循环结束;否则,Lua会执行循环体并不断地重复这个过程。
### 8.2.3 repeat
顾名思义,repeat-until语句会重复执行其循环体直到条件为真时结束。
由于条件测试在循环体之后执行,所以循环体至少会执行一次。

和大多数其他编程语言不同,在Lua语言中,循环体内声明的局部变量的作用域包括测试条件:

– 使用Newton-Raphson法计算’x’的平方根
local sqr = x/2
repeat
sqr = (sqr + x / sqr) / 2
local error = math.abs(sqr^2 - x)
until error < x/10000 – 局部变量’error’此时仍然可见

1
2
3
4
### 8.2.4 数值型for
for语句有两种形式:数值型(numerical)for和泛型(generic)for。

数值型for的语法如下:

for var = exp1,exp2,exp3 do
something
end

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
29
30
31
32
33
34
35
在这种循环中,var的值从exp1变化到exp2之前的每次循环会执行something,并在每次循环结束后将步长(step)exp3增加到var上。
第三个表达式exp3是可选的,若不存在,Lua语言会默认步长值为1。
如果不想给循环设置上限,可以使用常量math.huge。

为了更好地使用for循环,还需要了解一些细节。
首先,在循环开始前,三个表达式都会运行一次;
其次,控制变量是被for语句自动声明的局部变量,且其作用范围仅限于循环体内。

如果需要在循环结束后使用控制变量的值(通常在中断循环时),则必须将控制变量的值保存到另一个变量中。

最后,不要改变控制变量的值,随意改变控制变量的值可能产生不可预知的结果。
如果要在循环正常结束前停止for循环,可以使用break语句。
### 8.2.5 泛型for
泛型for遍历迭代函数返回的所有值,例如我们已经在很多示例中看到过的pairs、ipairs和io.lines等。
虽然泛型for看似简单,但它的功能非常强大。使用恰当的迭代器可以在保证代码可读性的情况下遍历几乎所有的数据结构。

与数值型for不同,泛型for可以使用多个变量,这些变量在每次循环时都会更新。当第一个变量变为nil时,循环终止。
像数值型for一样,控制变量是循环体中的局部变量,我们也不应该在循环中改变其值。
## 8. 3 break、return和goto
break和return语句用于从当前的循环结构中跳出,goto语句则允许跳转到函数中的几乎任何地方。

我们可以使用break语句结束循环,该语句会中断包含它的内层循环(例如for、repeat或者while);该语句不能在循环外使用。break中断后,程序会紧接着被中断的循环继续执行。
return语句用于返回函数的执行结果或简单地结束函数的运行。所有函数的最后都有一个隐含的return,因此我们不需要在每一个没有返还值的函数最后书写return语句。

goto语句用于将当前程序跳转到相应的标签处继续执行。
在Lua语言中,goto语句的语法非常传统,即保留字goto后面紧跟着标签名,标签名可以是任意有效的标识符。标签的语法稍微有点复杂:标签名称前后各紧跟两个冒号,形如::name::。
这个复杂的语法是有意而为的,主要是为了在程序中醒目地突出这些标签。

在使用goto跳转时,Lua语言设置了一些限制条件。
首先,标签遵循常见的可见性规则,因此不能直接跳转到一个代码块中的标签(因为代码块中的标签对外不可见)。
其次,goto不能跳转到函数外(注意第一条规则已经排除了跳转进一个函数的可能性)。
最后,goto不能跳转到局部变量的作用域。

关于goto语句典型且正确的使用方式,请参考其他一些编程语言中存在但Lua语言中不存在的代码结构,例如continue、多级break、多级continue、redo和局部错误处理等。
continue语句仅仅相当于一个跳转到位于循环体最后位置处标签的goto语句,而redo语句则相当于跳转到代码块开始位置的goto语句:

while some_condition do
::redo::

if some_other_condition then
    goto continue
else if yet_another_condition then
    goto redo
end

some code

::continue::

end

1
2
3

Lua语言规范中一个很有用的细节是,局部变量的作用域终止于声明变量的代码块中的最后一个有效(non-void)语句处,标签被认为是无效(void)语句。
下列代码展示了这个实用的细节:

while some_condition do
if some_other_condition then
goto continue
end

local var = something

some code

::continue::

end

1
2
3
4
5
6
读者可能认为,这个goto语句跳转到了变量var的作用域内。
但实际上这个continue标签出现在该代码块的最后一个有效语句后,因此goto并未跳转进入变量var的作用域内。

举一个简单的迷宫游戏的例子。
迷宫中有几个房间,每个房间的东南西北方向各有一扇门。玩家每次可以输入移动的方向,如果在这个方向上有一扇门,则玩家可以进入相应的房间,否则程序输出一个警告,玩家的最终目的是从第一个房间走到最后一个房间。
这个游戏是一个典型的状态机,当前玩家所在房间就是一个状态。为实现这个迷宫游戏,我们可以为每个房间对应的逻辑编写一段代码,然后用goto语句表示从一个房间移动到另一个房间。示例展示了如何编写一个由4个房间组成的小迷宫。

goto room1 – 起始房间

::room1:: do
local move = io.read()
if move == “south” then
goto room3
elseif move == “east” then
goto room2
else
print(“invalid move”)
goto room1 – 待在同一个房间
end
end

::room2:: do
local move = io.read()
if move == “south” then
goto room4
elseif move == “west” then
goto room1
else
print(“invalid move”)
goto room2
end
end

::room3:: do
local move = io.read()
if move == “north” then
goto room1
elseif move == “east” then
goto room4
else
print(“invalid move”)
goto room3 – 待在同一个房间
end
end

::room4:: do
print(“Congratulations,you won!”)
end