From《Programming in Lua》 by Roberto Ierusalimschy
字符串用于表示文本。Lua语言中的字符串既可以表示单个字符,也可以表示一整本书籍(超大字符序列)。Lua语言中的字符串是一串字节组成的序列。在Lua语言中,字符使用8个比特位来存储。Lua语言中的字符串可以存储包括空字符在内的所有数值代码,这意味着我们可以在字符串中存储任意的二进制数据。
Lua语言中的字符串是不可变值。我们不能像在C语言中那样直接改变某个字符串中的某个字符,但是我们可以通过创建一个新字符串的方式来达到修改的目的。
a = "one string"
b = string.gsub(a, "one", "another") -- 改变字符串中的某些部分
print(a) --> one string
print(b) --> another string
像Lua语言中的其他对象(表、函数等)一样,Lua语言中的字符串也是自动内存管理的对象之一。这意味着Lua语言会负责字符串的分配和释放,开发人员无须关注。
可以使用长度操作符(#)获取字符串的长度:
a = "hello"
print(#a) --> 5
print(#"good bye") --> 8
该操作符返回字符串占用的字节数,在某些编码中,这个值可能与字符串中字符的个数不同。
我们可以使用连接操作符…(两个点)来进行字符串连接。如果操作数中存在数值,那么Lua语言会先把数值转换成字符串:
"Hello " .. "World" --> Hello World
"result is " .. 3 --> result is 3
应该注意,在Lua语言中,字符串是不可变量。字符串连接总是创建一个新字符串,而不会改变原来作为操作数的字符串:
a = "Hello"
a .. " World" --> Hello World
a --> Hello
我们可以使用一对双引号号或单引号来声明字符串常量:
a = 'a line
b = 'another line'
使用双引号和单引号声明字符串是等价的。它们两者唯一的区别在于,使用双引号声明的字符串中出现单引号时,单引号可以不用转义;使用单引号声明的字符串中出现双引号时,双引号可以不用转义。
Lua 语言中的字符串支持下列C语言风格的转义字符:
转义字符 | 含义 |
---|---|
\a | 响铃(bell) |
\b | 退格(back space) |
\f | 换页(form feed) |
\n | 换行(newline) |
\r | 回车(carriage return) |
\t | 水平制表符(horizontal tab) |
\v | 垂直制表符(vertical tab) |
\\ | 反斜杠(backslash) |
\" | 双引号(double quote) |
\’ | 单引号(single quote) |
在字符串中,还可以通过转义序列\ddd和\xhh来声明字符。其中,ddd是由最多3个十进制数字组成的序列,hh是由两个且必须是两个十六进制数字组成的序列。
在一个使用 ASCII 编码的系统中,”AL0\n123\"“和’\x41L0\10\04923” '实际上是一样 0x41(十进制的65)在ASCII 编码中对应A,10对应换行符,49 对应数字1(在这个例子中,由于转义序列之后紧跟了其他的数字,所以 49 必须写成\049,即用0来补足三位数字;否则,Lua 语言会将其错误地解析为\492)。
我们还可以把上述字符串写成 '\x41\x4c\x4f\x0a\x31\x32\x33\x22 ’ ,即使用十六进制来表示字符串中的每一个字符。
像长注释/多行注释一样,可以使用一对双方括号来声明长字符串/多行字符串常量。被方括号括起来的内容可以包括很多行,并且内容中的转义序列不会被转义。此外,如果多行字符串中的第一个字符是换行符,那么这个换行符会被忽略。多行字符串在声明包含大段代码的字符串时非常方便,例如:
page = [[
An HTML Page
Lua
]]
write(page)
有时字符串中可能有类似a=b[c[i]]这样的内容(注意其中的]]),或者,字符串中可能有被注释掉的代码。为了应对这些情况,可以在两个左方括号之间加上任意数量的等号,如[===[。这样,字符串常量只有在遇到了包含相同数量等号的两个右方括号时才会结束(就前例而言,即]===])。Lua 语言的语法扫描器会忽略所含等号数量不相同的方括号。通过选择恰当数量的等号,就可以在无须修改原字符串的情况下声明任意的字符串常量了。
对注释而言,这种机制也同样有效。例如,我们可以使用–[=[和]=]来进行长注释,从而降低了对内部已经包含注释的代码进行注释的难度。
Lua语言在运行时提供了数值与字符串之间的自动转换。针对字符串的所有算术操作会尝试将字符串转换为数值。Lua 语言不仅仅在算术操作时进行这种强制类型转换,还会在任何需要数值的情况下进行,例如函数 math.sin 参数。
相反,当 Lua 语言发现在需要字符串的地方出现了数值时,它就会把数值转换为字符串:
print(10 .. 20) --> 1020
当在数值后紧接着使用字符串连接时,必须使用空格将它们分开,否则Lua语言会把第一个点当成小数点。
Lua 5.3没有实现强制类型转换与整型的集成,而是采用了另一种更简单和快速的实现方式:算术运算的规则就是只有在两个操作数都是整型值时结果才是整型,由于字符串不是整型值,所以任何有字符串参与的算术运算都会被当作浮点运算处理:
"10" + 1 --> 11.0
如果需要显式地将一个字符串转换成数值,那么可以使用函数 tonumber。当这个字符串的内容不能表示为有效数字时该函数返回 nil。否则,该函数就按照Lua语法扫描器的规则返回对应的整型值或浮点类型值:
tonumber(" -3") --> -3
tonumber(" 10e4 ") --> 100000.0
tonumber("10e") --> nil (not a valid number)
tonumber("0x1.3p-4") --> 0.07421875
默认情况,函数tonumber使用的是十进制,但是也可以指明使用二进制到三十六进制之间的任意进制:
tonumber("100101", 2) --> 37
tonumber("fff", 16) --> 4095
tonumber("-ZZ", 36) --> -1295
tonumber("987", 8) --> nil
在最后一行中,对于指定的进制而言,传入的字符串是一个无效值,因此函数tonumber返回nil。
调用函数tostirng可以将数值转换成字符串:
print(tostring(10) == "10") --> true
上述的这种转换总是有效,但我们需要记住,使用这种转换时并不能控制输出字符串的格式(例如,结果中十进制数字的个数)。可以通过函数string.format来全面地控制输出字符串的格式。下节会详细介绍。
与算术操作不同,比较操作符不会对操作数进行强制类型转换。请注意,“0”和0是不同的。此外,2<15明显为真,但“2” < "15"却为假(字母顺序)。为了避免出现不一致的结果,当比较操作符中混用了字符串和数值(比如如2 < “15”)时,Lua 语言会抛出异常。
Lua语言解释器本身处理字符串的能力是十分有限的。一个程序能够创建字符串、连接字符串、比较字符串和获取字符串的长度,但是,它并不能提取字符串的子串或检视字符串的内容。Lua语言处理字符串的完整能力来自其字符串标准库。
正如此前提到的,字符串标准库默认处理的是8bit (1 byte)字符。这对于某些编码方式(例如 ASCII或ISO-8859-1)适用,但对所有的 Unicode 编码来说都不适用。
字符串标准库中的一些函数非常简单:
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.lower(a) < string.lower(b)
函数string.sub(s, i, j)从字符串s中提取第i个到第j个字符(包括第i个和第j个字符,字符串的第一个字符索引为1)。该函数也支持负数索引,负数索引从字符串的结尾开始计数:索引-1代表字符串的最后一个字符,索引-2代表倒数第二个字符,依此类推。以下为常用法:
s = "[in brackets]"
string.sub(s, 2, -2) --> in brackets
string.sub(s, 1, 1) --> [
string.sub(s, -1, -1) --> ]
如果果需要修改原字符串,那么必须把新的值赋值给它:
s = string.sub(s, 2, -2)
函数string.char和string.byte于转换字符及其内部数值表示。
在下例中,假定字符是用ASCII表示的:
print(string.char(97)) --> a
i = 99;print(stirng.char(i, i+1, i+2)) --> cde
print(string.byte("abc")) --> 97
print(string.byte("abc", 2)) --> 98
print(string.byte("abc", -1)) --> 99
print(string.byte("abc", 1, 2)) --> 97 98
一种常见的写法是 {string.byte(s, 1, -1)} ,该表达式会创建一个由字符串s中的所有字符代码组成的表(由Lua语言限制了栈大小,所以也限制了一个函数的返回值的最大个数,默认最大为一百万个。因此,这个技巧不能用于大小超过1MB字符串)。
函数string.format是用于进行字符串格式化和将数值输出为字符串的强大工具,与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) --> title
在百分号和字母之间可以包含用于控制格式细节的其他选项。例如,可以指定一个浮点数中小数点的位数:
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
可以使用冒号操作符像调用字符串的一个方法那样调用字符串标准库中的所有函数。
string.sub(s, i, j) --> s:sub(i, j)
string.upper(s) --> s:upper()
字符串标准库还包括了几个基于模式匹配的函数:
string.find("hello world", "wor") --> 7 9
string.find("hello world", "war") --> nil
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
在后续章节我们会对模式匹配函数进行详细深入的讲解。
表(Table)是Lua语言中最主要(事实上也是唯一的)和强大的数据结构。使用表, Lua语言可以以一种简单、统一且高效的方式表示数组、集合、记录和其他很多数据结构。Lua语言也使用表来表示包( package )和其他对象。当调用函数 math.sin,我们可能认为是“调用了 math 库中函数 sin”,而对Lua语言来说,其实际含义是”以字符串sin为键检索表math”。
Lua语言中的表本质上是一种辅助数组,这种数组不仅可以使用数值作为索引,也可以使用字符串或其他任意类型的值作为索引(nil 除外)。
Lua语言中的表要么是值要么是变量,它们都是对象。可以认为,表是一种动态分配的对象,程序只能操作指向表的引用(或指针) 。除此以外, Lua语言不会进行隐藏的拷贝或创建新的表。
我们使用构造器表达式创建表,其最简单的形式是{}:
a = {} -- 创建一个表然后用表的引用赋值
k = "x"
a[k] = 10 -- 新元素,键是“x”,值是10
a[20] = "great" -- 新元素,键是20,值是“great”
a["x"] --> 10
k = 20
a[k] --> great
a["x"] = a["x"] + 1 -- 增加元素“x”的值
a["x"] --> 11
表永远是匿名的,表本身和保存表的变量之间没有固定的关系:
a = {}
a["x"] = 10
b = a -- 'b'和'a'引用同一张表
b["x"] --> 10
b["x"] = 20
a["x"] --> 20
a = nil -- 只有'b'仍然指向表
b = nil -- 没有指向表的引用了
对于一个表而言,当程序中不再有指向它的引用时,垃圾收集器会最终删除这个表并重用其占用的内存。
同一个表中存储的值可以具有不同类型的索引,并可以按需增长以容纳新的元素。
a = {} -- 空表
for i = 1, 1000 do a[i] = i * 2 end -- 创建1000个新元素
a[9] --> 18
a["x"] = 10
a["x"] --> 10
a["y"] --> nil
如同全局变量一样,未经初始化的表元素为nil,将nil值给表元素可以将其删除。这并非巧合,因为Lua语言实际上就是使用表来存储全局变量的。
当把表当作结构体使用时,可以把索引当作成员名称使用 (a. name 等价于 a[“name”])。因此,可以使用这种更易读的方式改写前述示例的最后几行。
a = {} -- 空表
a.x = 10 -- 等价于 a["x"] = 10
a.x -- 等价于 a["x"]
a.y -- 等价于 a["y"]
形如a.name的点分形式清晰地说明了表是被当作结构体使用的,此时表实际上是由固定的、预先定义的键组成的集合;而形如 a[“name”]的字符串索形式则说明了表可以使用任意字符串作为键,并且出于某种原因我们操作的是指定的键。
实际上,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“的值
由于可以使用任意类型索引表,所以在索引表时会遇到相等性比较方面的微妙问题。虽然确实都能用数字0和字符串”0“对同一个表进行索引,但这两个索引的值及其所对应的元素是不同的。同样,字符串”+1”、“01”和“1”指向的也是不同的元素。当不能确定表索引的真实数据类型时,可以使用显式的类型转换:
i = 10; j = "10"; k = "+10"
a = {}
a[i] = "number key"
a[j] = "string key"
a[k] = "another stirng key"
a[i] --> number key
a[j] --> string key
a[k] --> another stirng key
a[tonumber(j)] --> number key
a[tonumber(k)] --> number key
整型和浮点型类型的表索引则不存在上述问题。由于2和2.0的值相等,所以当它们被当作表索引使用时指向的是同一个表元素:
a = {}
a[2.0] = 10
a[2.1] = 20
a[2] --> 10
a[2.1] --> 20
更准确地说,当被用作表索引时,任何能够被转换为整型的浮点数都会被转换成整型数。例如,当执行表达式a[2.0]=10时,键2.0会被转换为2。相反,不能被转换为整型数的浮点数则不会发生上述的类型转换。
表构造器是用来创建和初始化表的表达式,也是Lua语言中独有的也是最有用、最灵活的机制之一。
最简单的构造器是空构造器{},构造器也可以被用来初始化列表。
days = {"Sunday", "Monday", "Tuesday", "Wednesday",
"Thursday", "Friday", "Saturday"}
print(days[4]) --> Wednesday
Lua语言还提供了一种初始化记录式表的特殊语法:
a = {x = 10, y = 20}
上述代码等价于:
a = {}
a.x = 10
a.y = 20
在一种写法中,由于能够提前判断表的大小,因此运行速度更快。
无论使用哪种方式创建表,都可以随时增加或删除表元素:
w = {x = 0, y = 0, label = "console"}
x = {math.sin(0), math.sin(1), math.sin(2)}
w[1] = "another field" -- 把键1增加到表'w'中
x.f = w -- 把键“f”增加到表'x'中
print(w["x"]) --> 0
print(w[1]) --> another field
print(x.f[1]) --> another field
w.x = nil --> 删除字段“x”
在同一个构造器中,可以混用记录式和列表式写法:
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]
}
上述的示例也同时展示了如何创建嵌套表(和构造器)以表达更加复杂的数据结构。每一个元素polyline[i]都是代表一条记录的表:
print(polyline[2].x) --> -10
print(polyline[4].y) --> 1
另一种更加通用的构造器是通过方括号括起来的表达式显式地指定每一个索引:
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]) --> ---
这种构造器非常灵活,不管是记录式构造器还是列表式构造器均是其特殊形式。例如,下面的几种表达式就相互等价:
{x = 0, y = 0} <==> {["x"] = 0, ["y"] = 0}
{"r", "g", "b"} <==> {[1] = "r", [2] = "g", [3] = "b"}
如果想表示常见的数组(array)或列表(list),那么只需要使用整型作为索引的表即可。同时,也不需要预先声明表的大小,只需要直接初始化我们需要的元素即可:
-- 读取10行,然后保存在一个表中
a = {}
for i = 1, 10 do
a[i] = io.read()
end
在 Lua 语言中,数组索引按照惯例是从1开始的(不像C语言从0开始), Lua语言中的其他很多机制也遵循这个惯例。
由于未初始化的元素均为nil,所以可以利用nil值来标记列表的结束。例如,当向一个列表中写入了10行数据后,由于该列表的数值类型的索引为1,2,…,10,所以可以很容易地知道列表的长度就是10。这种技巧只有在列表中不存在空洞(hole)时(即所有元素均不为nil)才有效,此时我们把这种所有元素都不为nil的数组称为序列。
Lua语言提供了获取序列长度的操作符#。对于字符串而言,该操作符返回字符串的字节数;对于表而言,该操作符返回表对应序列的长度。例如,可以使用如下的代码输出上例中读入的内容:
-- 输出行,从1到#a
for i = 1, #a do
print(a[i])
end
长度操作符也为操作序列提供了几种有用的写法:
print(a[#a]) -- 输出序列‘a’的最后一个值
a[#a] = nil -- 移除最后一个值
a[#a + 1] = v -- 把‘v’加到序列的最后
使用pairs迭代器遍历表中的键值对:
t = {10, print, x = 12, k = "hi"}
for k,v in pairs(t) do
print(k, v)
end
--> 1 10
--> 2 function: 0x10e052800
--> k hi
--> x 12
受限于表在Lua语言中的底层实现机制,遍历过程中元素的出现顺序可能是随机的 ,相同的程序在每次运行时也可能产生不同的顺序。唯一可以确定的是,在遍历的过程中每个元素会且只会出现一次。
对于列表而言,可以使用ipairs迭代器,此时,Lua会确保遍历是按照顺序进行的:
t = {10, print, 12, "hi"}
for k,v in ipairs(t) do
print(k, v)
end
--> 1 10
--> 2 function: 0x10622a800
--> 3 12
--> 4 hi
另一种遍历序列的方法是使用数值型for循环:
t = {10, print, 12, "hi"}
for k = 1, #t do
print(k, t[k])
end
--> 1 10
--> 2 function: 0x10622a800
--> 3 12
--> 4 hi
考虑如下的情景:我们想确认在指定的库中是否存在某个函数。如果我们确定这个库确实存在,那么可以直接使用 if lib.too then …;否则,就得使用形如 if lib and lib.foo then …的表达式。
当表的嵌套深度变得比较深时,这种写法就会很容易出错,例如:
zip = company and company.director and
company.director.address and
company.director.address.zipcode
这种写法不仅冗长而且低效,该写法一次成功的访问中对表进行了6次访问而非3次访问。
对于这种情景,诸如C#的一些编程语言提供了一种安全访问操作符。在C#中,这种安全访问操作符被记为"?."。例如,对于表达式a?.b,当a为nil时,其结果是nil而不会产生异常。可讲上例改写为:
zip = company?.director?.address?.zipcode
Lua语言并没有提供安全访问操作符,但是我们可以使用其他语句在Lua语言中模拟安全访问操作符。
对于表达式a or {},当a为nil时其结果是一个空表。因此,对于表达式(a or {}).b,当a为nil时结果也同样是nil。这样,我们就可以将之前的例子重写为:
zip = (((company or {}).director or{}).address or {}).zipcode
再进一步,我们还可以写的更短、更高效:
E = {}
...
zip = (((company or E).director or E).address or E).zipcode
确实,上述的语法比安全访问操作符更加复杂。不过尽管如此,表中的每一个字段名都只被使用了一次,从而保证了尽可能少地对表进行访问。
表标准库提供了操作列表和序列的一些常用函数。
函数table.insert向序列的指定位置插入一个元素,其他元素依次后移。例如,对于列表t = {10, 20, 30},在调用table.insert(t, 1, 15)后它会变成{15, 10, 20, 30},另一种特殊但常见的情况是调用insert时不指定位置,此时该函数会在序列的最后插入指定的元素,而不会移动任何元素。
例如,下述代码从标准输入中按行读入内容并将其保存到一个序列中:
t = {}
for line in io.lines() do
table.insert(t, line)
end
print(#t) --> 读取的行数
函数 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
如下代码可以在表a的开头插入一个元素:
table.move(a, 2, #a, 1)
a[#a] = nil
应该注意,move实际上是将一个值从一个地方拷贝到另一个地方。因此,像上面的例子一样,我们必须在移动后显式地把最后一个元素删除。
函数 table.move 还支持使用一个表作为可选的参数。当带有可选的表作为参数 ,该函数将第一个表中的元素移动到第二个表中。例如,table.move(a, 1, #a, 1, {}) 返回列表a的一个克隆(通过将列表a中的所有元素拷贝到新列表中),table.move(a, 1, #a, #b + 1, b) 将列表a中的所有元素复制到列表b的末尾。
在Lua语言中,函数( Function )是对语句和表达式进行抽象的主要方式。函数既可以用于完成某种特定任务,也可以只是进行一些计算然后返回计算结果。在前一种情况下,我们将一句函数调用视为一条语句;而在后一种情况下,我们则将函数调用视为表达式:
print(8*9, 9/8)
a = math.sin(3) + math.cos(10)
print(os.date())
无论哪种情况,函数调用时都需要使用一对圆括号把参数列表括起来。即使被调用的函数不需要参数,也需要一对空括号()。对于这个规则,唯一的例外就是,当函数只有一个参数且该参数是字符串常量或表构造器时,括号时可选的:
print "Hello World" -- print("Hello World")
dofile 'a.lua' -- dofile('a.lua')
print [[a multi-line message]] -- print([[a multi-line message]])
f{x=10, y=10} -- f({x=10, y=20})
type{} -- type({})
Lua语言也为面向对象风格的调用提供了一种特殊的语法,即冒号操作符。形如o:foo(x)的表达式意为调用对象o的foo方法。
一个Lua程序既可以调用Lua语言编写的函数,也可以调用C语言(或者宿主程序使用的其他任意语言)编写的函数。一般来说,我们选择使用C语言编写的函数来实现对性能要求更高,或不容易直接通过Lua语言进行操作的操作系统机制等。例如,Lua语言标准库中所有的函数就都是使用C语言编写的。不过,无论一个函数是用Lua语言编写的还是用C语言编写的,在调用它们时都没有任何区别。
正如我们已经在其他示例中所看到的,Lua语言中的函数定义的常见语法格式形如:
-- 对序列'a'中的元素求和
function add (a)
local sum = 0
for i = 1, #a do
sum = sum + a[i]
end
return sum
end
在这种语法中,一个函数定义具有一个函数名、一个参数组成的列表和由一组语句组成的函数体。参数的行为与局部变量的行为完全一致,相当于一个用函数调用时传入的值进行初始化的局部变量。
调用函数时使用的参数个数可以与定义函数时使用的参数个数不一致。Lua语言会通过抛弃多余参数和将不足的参数设为nil的方式来调整参数的个数。例如,考虑如下的函数:
function f(a, b) print(a, b) end
其行为如下:
f() --> nil nil
f(3) --> 3 nil
f(3, 4) --> 3 4
f(3, 4, 5) --> 3 4 (5被丢弃)
虽然这种行为可能导致编程错误(在单元测试中容易发现),但同样又是有用的,尤其是对于默认参数的情况。例如,考虑如下递增全局计数器的函数:
function incCount (n)
n = n or 1
globalCounter = globalCounter + n
end
该函数以1作为默认实参,当调用无参数的incCount时,将globalCounter加1。在调用incCount()时,Lua语言首先把参数n初始化为nil,接下来的or表达式又返回了其第二个操作数,最终把n赋成了默认值1。
Lua语言中一种与众不同但又非常有用的特性是允许一个函数返回多个结果。Lua语言中几个预定义函数就会返回多个值。我们已经接触过函数string.find,该函数用于在字符串中定位模式。当找到了对应的模式时,该函数会返回两个索引值:所匹配模式在字符串中起始字符和结尾字符的索引。使用多重赋值可以同时获取到这两个结果:
s, e = string.find("hello Lua Users", "Lua")
print(s, e) --> 7 9
注意,字符串首字符的索引值为1。
Lua语言编写的函数同样可以返回多个结果,只需在return关键字后列出所有要返回的值即可。例如,一个用于查找序列中最大元素的函数可以同时返回最大值及该元素的位置:
function maximum (a)
local mi = 1 -- 最大值的索引
local m = a[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})) --> 23 3
Lua语言会根据函数的被调用情况调整返回值的数量:
function foo0() end -- 不返回结果
function fool() return "a" end -- 返回1个结果
function foo2() return "a","b" end -- 返回2个结果
在多重赋值中,如果一个函数调用是一系列表达式中的最后(或者是唯一)一个表达式,则该函数调用将产生尽可能多的返回值以匹配待赋值变量:
x, y = foo2() -- x = "a", y = "b"
x = foo2() -- x = "a", 返回值"b"被丢弃
x, y, z = 10, foo2() -- x = 10, y = "a", z = "b"
在多重赋值中,如果一个函数没有返回值或者返回值个数不够多,那么Lua语言会用nil来补充缺失的值:
x, y = foo0() -- x = nil, y = nil
x, y = fool() -- x = "a", y = nil
x, y, z = foo2() -- x = "a, y = "b', z = nil
请注意,只有当函数调用是一系列表达式中的最后(或者是唯一)一个表达式时才能返回多值结果,否则只能返回一个结果:
x, y = foo2(), 20 -- x = "a', y = 20 ('b'被丢弃)
x, y = foo0(), 20, 30 -- x = nil, y = 20 (30被丢弃)
当一个函数调用是另一个函数调用的最后一个(或者是唯一)实参时,第一个函数的所有返回值都会被作为实参传给第二个函数。我们已经见到过很多这样的代码结构,例如函数print。由于函数print能够接收可变数量的参数,所以print(g())会打印出g返回的所有结果。
print(foo0()) --> (没有结果)
print(foo1()) --> a
print(foo2()) --> a b
print(foo2(), 1) --> a 1
print(foo2() .. "x") --> ax
当在表达式中调用和foo2时,Lua语言会把其返回值的个数调整为1。因此,在上例的最后一行,只有第一个返回值”a“参与了字符串连接操作。
当我们调用f(g())时,如果f的参数是固定的,那么Lua语言会把g返回值的个数调整成与f的参数个数一致。这并非巧合,实际上这正是多重赋值的逻辑。
表构造器会完整地接收函数调用的所有返回值,而不会调整返回值的个数:
t = {foo0()} --> t = {}
t = {foo1()} --> t = {"a"}
t = {foo2()} --> t = {"a", "b"}
不过,这种行为只有当函数调用是表达式列表中的最后一个时才有效,在其他位置上的函数调用总是只返回一个结果:
t = (foo0(), foo2(), 4) -- t[1] = nil, t[2] = "a", t[3] = 4
最后,形如return f()的语句会返回f返回的所有结果:
function foo (i) -
if i == 0 then return foo0()
elseif i == 1 then return fool()
elseif i == 2 then return foo2()
end
end
print(foo(1)) --> a
print(foo(2)) --> a b
print(foo(0)) -- (无结果)
print(foo(3)) -- (无结果)
将函数调用用一对圆括号括起来可以强制其只返回一个结果:
print((foo0())) --> nil
print((foo1())) --> a
print((foo2())) --> a
同样的,无论函数f究竟返回几个值,形如return(f(x))的语句只返回一个值。
Lua语言中的函数可以是可变长参数函数,即可以支持数量可变的参数。例如,我们已经使用一个、两个或更多个参数调用过函数print。虽然函数print是在C语言中定义的,但也可以在Lua语言中定义可变长参数函数。
下面是一个简单的示例,该函数返回所有参数的总和:
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
参数列表中的三个点(…)表示该函数的参数是可变长的。当这个函数被调用时,Lua内部会把它的所有参数收集起来,我们把这些被收集起来的参数称为函数的额外参数。当函数要访问这些参数时仍需用到三个点,但不同的是此时这三个点是作为一个表达式来使用的。在上例中,表达式{…}的结果是一个由所有可变长参数组成的列表,该函数会遍历该列表来累加其中的元素。
我们将三个点组成的表达式称为可变长参数表达式,其行为类似于一个具有多个返回值的函数,返回的是当前函数的所有可变长参数。例如,print(…)会打印出该函数的所有参数。又如,如下的代码创建了两个局部变量,其值为前两个可选的参数(如果参数不存在则为nil):
local a,b = ...
实际上,可以通过变长参数来模拟Lua语言中普通的参数传递机制,例如:
function foo (a, b, c)
可以写成:
function foo (...)
local a, b, c = ...
Lua语言提供了专门用于格式化输出的函数string.format和输出文本的函数io.write。我们会很自然地想到把这两个函数合并为一个具有可变长参数的函数:
function fwrite (fmt, ...)
return io.write(string.format(fmt, ...))
end
注意,在三个点前有一个固定的参数fmt。具有可变长参数的函数也可以具有任意数量的固定参数,但固定参数必须放在变长参数之前。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 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
另一种遍历函数的可变长参数的方法是使用函数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的地方使用函数select,因此可以把select(n, …)认为是返回第n个额外参数的表达式。
来看一个使用函数select的典型示例,下面是使用该函数的add函数:
function add (...)
local s = 0
for i = 1, select("#", ...) do
s = s + select(i, ...) -- 每次只会保留一个返回值
end
return s
end
对于参数较少的情况,第二个版本的add更快,因为该版本避免了每次调用时创建一个新表。不过,对于参数较多的情况,多次带有很多参数调用函数select会超过创建表的开销,因此第一个版本会更好。
多重返回值还涉及一个特殊的函数table.unpack。该函数的参数是一个数组,返回值为数组内的所有元素:
print(table.unpack(10,20,30}) --> 10 20 30
a,b = table.unpack{10,20,30} --> a=10, b=20, 3被丢弃
顾名思义,函数table.unpack与函数table.pack的功能相反。
unpack函数的重要用途之一体现在泛型调用机制中。泛型调用机制允许我们动态地调用具有任意参数的任意函数。例如,在ISO C中,我们无法编写泛型调用的代码,只能声明可变长参数的函数(使用stdarg.h)或使用函数指针来调用不同的函数。但是,我们仍然不能调用具有可变数量参数的函数,因为C语言中的每一个函数调用的实参个
数是固定的,并且每个实参的类型也是固定的。而在Lua语言中,却可以做到这一点。如果我们想通过数组a传入可变的参数来调用函数f,那么可以写成:
f(table.unpack(a))
unpack会返回a中所有的元素,而这些元素又被用作f的参数。例如,考虑如下的代码:
print(string.find("hello","ll"))
可以使用如下的代码动态地构造一个等价的调用:
f = string.find
a = {"hello", "ll"}
print(f(table.unpack(a)))
通常,函数table.unpack使用长度操作符获取返回值的个数,因而该函数只能用于序列。不过,如果有需要,也可以显式地限制返回元素的范围:
print(table.unpack({"Sunn", "Mon", "Tuen", "Wed"}, 2, 3))
--> Mon Tue
Lua语言中有关函数的另一个有趣的特性是,Lua语言支持尾调用消除。这意味着Lua语言可以正确地尾递归。
尾调用是被当作函数调用使用的跳转。当一个函数的最后一个动作是调用另一个函数而没有再进行其他工作时,就形成了尾调用。例如,下列代码中对函数g的调用就是尾调用:
function f (x) x = x + 1; return g(x) end
当函数f调用完函数g之后,f不再需要进行其他的工作。这样,当被调用的函数执行结束后,程序就不再需要返回最初的调用者。因此,在尾调用之后,程序也就不需要在调用栈中保存有关调用函数的任何信息。当g返回时,程序的执行路径会直接返回到调用f的位置。
在一些语言的实现中,例如Lua语言解释器,就利用了这个特点,使得在进行尾调用时不使用任何额外的栈空间。我们就将这种实现称为尾调用消除。
由于尾调用不会使用栈空间,所以一个程序中能够嵌套的尾调用的数量是无限的。例如,下列函数支持任意的数字作为参数:
function foo (n)
if n > 0 then return foo(n - 1)
end
该函数永远不会发生栈溢出。
关于尾调用消除的一个重点就是如何判断一个调用是尾调用。很多函数调用之所以不是尾调用,是由于这些函数在调用之后还进行了其他工作。例如,下例中调用g就不是尾调用:
function f(x) g(x) end
这个示例的问题在于,当调用完g后,f在返回前还不得不丢弃g返回的所有结果。类似的,以下的所有调用也都不符合尾调用的定义:
return g(x) + 1 -- 必须进行加法
return x or g(x) -- 必须把返回值限制为1个
return (g(x)) -- 必须把返回值限制为1个
在Lua语言中,只有形如return func(args)的调用才是尾调用。不过,由于Lua语言会在调用前对func及其参数求值,所以func及其参数都可以是复杂的表达式。例如,下面的例子就是尾调用:
return x[i].foo(x[j] + a*b, i + j)
由于Lua语言强调可移植性和嵌入性,所以Lua语言本身并没有提供太多与外部交互的机制。在真实的Lua程序中,从图形、数据库到网络的访问等大多数I/O操作,要么由宿主程序实现,要么通过不包括在发行版中的外部库实现。单就Lua语言而言,只提供了ISO C语言标准支持的功能,即基本的文件操作等。
对于文件操作来说,I/O库提供了两种不同的模型。简单模型虚拟了一个当前输入流和一个当前输出流, 其I/O操作是通过这些流实现的。I/O库把当前输入流初始化为进程的标准输入(C语言中的stdin),将当前输出流初始化为进程的标准输出(C语言中的stdout)。因此,当执行类似于io.read()这样的语句时,就可以从标准输入中读取一行。
函数io.input和函数io.output可以用于改变当前的输入输出流。调用io.input(file-name)会以只读模式打开指定文件,并将文件设置为当前输入流。之后,所有的输入都将来自该文件,除非再次调用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。
函数io.write在将数值转换为字符串时遵循一般的转换规则;如果想要完全地控制这种转换,则应该使用函数string.format:
io.write("sin(3) = ", math.sin(3), "n\n")
--> sin(3) = 0.14112000805987
io.write(string.format("sin(3) = %.4f\n", math.sin(3)))
--> sin(3) = 0.1411
函数io.read可以从当前输入流中读取字符串,其参数决定了要读取的数据:
参数 | 含义 |
---|---|
“a” | 读取整个文件 |
“l” | 读取下一行(丢弃换行符) |
“L” | 读取下一行(保留换行符) |
“n” | 读取一个数值 |
Num | 以字符串读取num个字符 |
调用io.read(“a”)可从当前位置开始读取当前输入文件的全部内容。如果当前位置处于文件的末尾或文件为空,那么该函数返回一个空字符串。
因为Lua语言可以高效地处理长字符串,所以在Lua语言中编写过滤器的一种简单技巧就是将整个文件读取到一个字符串中,然后对字符串进行处理,最后输出结果为:
t = io. read ("a") -- 读取整个文件
t = string.gsub(t, "bad", "good") -- 进行处理
io.write(t) -- 输出结果
作为面向行的输入的一个简单例子,以下的程序会在将当前输入复制到当前输出中的同时对每行进行编号:
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迭代器会更简单:
local count = 0
for line in io.lines() do
count = count + 1
io.write(string.format("%6d ", count), line, "\n")
end
另一个面向行的输入的例子如下,给出了一个对文件中的行进行排序的完整程序。
local lines = {)
--将所有行读取到表'lines'中
for line in io.lines() do
lines[#lines + 1] = line
end
--排序
table.sort(lines)
--输出所有的行
for _, l in ipairs(lines) do
io.write(l,"\n")
end
调用io.read(“n”)会从当前输入流中读取一个数值,这也是函数read返回值为数值(整型或者浮点型,与Lua语法扫描器的规则一致)而非字符串的唯一情况。如果在跳过了空格后,函数io.read仍然不能从当前位置读取到数值(由于错误的格式问题或到了文件末尾),则返回nil。
除了上述这些基本的读取模式外,在调用函数read时还可以用一个数字n作为其参数:在这种情况下,函数read会从输入流中读取n个字符。如果无法读取到任何字符(处于文件末尾)则返回nil;否则,则返回一个由流中最多n个字符组成的字符串。以下的代码展示了将文件从stdin复制到stdout的高效方法:
while true do
local block = io.read(2^13) --块大小是8KB
if not block then break end
io.write(block)
end
io.read(0)是一个特例,它常用于测试是否到达了文件末尾。如果仍然有数据可供读取,它会返回一个空字符串;否则,则返回nil。
调用函数read时可以指定多个选项,函数会根据每个参数返回相应的结果。假设有一个每行由3个数字组成的文件:
6.0 -3.23 15e12
4.3 234 1000001
如果想打印每一行的最大值,那么可以通过调用函数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
简单I/O模型对简单的需求而言还算适用,但对于诸如同时读写多个文件等更高级的文件操作来说就不够了。对于这些文件操作,我们需要用到完整I/O模型。
可以使用函数io.open来打开一个文件,该函数仿造了C语言中的函数fopen。这个函数有两个参数,一个参数是待打开文件的文件名,另一个参数是一个模式(mode)字符串。模式字符串包括:
函数io.open返回对应文件的流。当发生错误时,该函数会在返回nil的同时返回一条错误信息及一个系统相关的错误码:
print(io.open("non-existent-file", "r"))
--> nil non-existent-file: No such file or directory 2
print(io.open("/etc/passwd", "w"))
--> nil /etc/passwd: Permission denied 13
检査错误的一种典型方法是使用函数assert:
local f = assert(io.open(filename, mode))
如果函数io.open执行失败,错误信息会作为函数assert的第二个参数被传入,之后函数assert会将错误信息展示出来。
在打开文件后,可以使用方法read和write从流中读取和向流中写入。它们与函数read和write类似,但需要使用冒号运算符将它们当作流对象的方法来调用。例如,可以使用如下的代码打开一个文件并读取其中所有内容:
local f = assert(io.open(filename, "r"))
local t = f:read("a")
f:close()
I/O库提供了三个预定义的C语言流的句柄:io.stdin、io.stdout和io.stderr。例如,可以使用如下的代码将信息直接写到标准错误流中:
io.stderr:write(message)
函数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) -- 恢复此前的当前输入流
注意,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当作句柄的一个方法。此外,从Lua5.2开始,函数io.lines可以接收和函数io.read一样的参数。
例如,下面的代码会以在8KB为块迭代,将当前输入流中的内容复制到当前输出流中:
for block in io.input():lines(2^13) do
io.write(block)
end
函数io.tmpfile返回一个操作临时文件的句柄,该句柄是以读/写模式打开的。当程序运行结束后,该临时文件会被自动移除(删除)。
函数flush将所有缓冲数据写入文件。与函数write一样,我们也可以把它当作io.flush()使用,以刷新当前输出流;或者把它当作方法f:flush()使用,以刷新流f。
函数setvbuf用于设置流的缓冲模式。该函数的第一个参数是一个字符串:
对于后两个选项,函数setvbuf支持可选的第二个参数,用于指定缓冲区大小。
在大多数系统中,标准错误流(io.stderr)是不被缓冲的,而标准输出流(io.stdout)按行缓冲。因此,当向标准输出中写入了不完整的行(例如进度条)时,可能需要刷新这个输出流才能看到输出结果。
函数seek用来获取和设置文件的当前位置,常常使用f:seek(whence, offset)的形式来调用,其中参数whence是一个指定如何使用偏移的字符串。
不管whence的取值是什么,该函数都会以字节为单位,返回当前新位置在流中相对于文件开头的偏移。
因此,有以下三种常用法:
下面的函数演示了如何在不修改当前位置的情况下获取文件大小:
function fsize (file)
local current = file:seek() -- 保存当前位置
local size = file:seek("end") -- 获取文件大小
file:seek("set", current) -- 恢复当前位置
return size
end
此外,函数os.rename用于文件重命名,函数os.remove用于移除(删除)文件。需要注意的是,由于这两个函数处理的是真实文件而非流,所以它们位于os库而非io库中。
上述所有的函数在遇到错误时,均会返回nil外加一条错误信息和一个错误码。
函数os.exit用于终止程序的执行。该函数的第一个参数是可选的,表示该程序的返回状态,其值可以为一个数值(0表示执行成功)或者一个布尔值(true表示执行成功);该函数的第二个参数也是可选的,当值为true时会关闭Lua状态并调用所有析构器释放所占用的所有内存(这种终止方式通常是非必要的,因为大多数操作系统会在进程退出时释放其占用的所有资源)。
函数os.getenv用于获取某个环境变量,该函数的输入参数是环境变量的名称,返回值为保存了该环境变量对应值的字符串:
print(os.getenv("HOME") --> /home/lua
对于未定义的环境变量,该函数返回nil。
函数os.execute用于运行系统命令,它等价于C语言中的函数system。该函数的参数为表示待执行命令的字符串,返回值为命令运行结束后的状态。
例如,在POSIX和Windows中都可以使用如下的函数创建新目录:
function createDir(dirname)
os.execute("mkdir " .. dirname)
end
另一个非常有用的函数是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
其中,函数io.popen的第二个参数"r"表示从命令的执行结果中读取。由于该函数的默认行为就是这样,所以在上例中这个参数实际是可选的。
函数os.execute和io.popen都是功能非常强大的函数,但它们也同样是非常依赖于操作系统的。
如果要使用操作系统的其他扩展功能,最好的选择是使用第三方库。比如用于基本目录操作和文件属性操作的LuaFileSystem,或者提供了POSIX.1标准支持的luaposix库。