Lua语言中的字符串既可以表示单个字符,也可以表示一个字符串。

Lua语言中,字符使用8个比特位来存储(与之对比的时7-bit ASCII)。Lua语言中的字符串可以存储包括空字符在内的所有数值代码,这意味着我们可以在字符串中存储任意的二进制数据。Lua的字符串标准库默认处理8个比特位(1Byte)的字符,此外,从Lua5.3开始还提供了一个帮助使用UTF-8编码的函数库。

Lua语言中的字符串是不可变的,但是我们可以通过创建一个新字符串的方式来达到修改的目的。例如:

a = "one string"
b = string.gsub(a, "one", "another")	-- 改变字符串中的某些部分
print(a)
print(b)

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

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

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

我们可以使用连接操作符..(两个点)来进行字符串连接。如果操作数中存在数值,那么Lua语言会先把数值转换成字符串:

"Hello " .. "World"		--> Hello World
"result is " .. 3		--> result is 3

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

4.1 字符串常量

我们可以使用一对双引号或单引号来声明字符串常量:

a = "a line"
b = 'another line'

使用双引号和单引号声明的字符是等价的。它们两者唯一的区别在于,使用双引号声明的字符串中出现单引号时,单引号可以不用转义;使用单引号,出现双引号,不用转义。

比如,与由于XML文本中一般会有双引号,所以操作XML的库可能就会使用单引号来声明XML片段。

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是最多三个十进制数字组成的序列,hh是两个且必须两个十六进制数字组成的序列。在一个使用ASCII编码的系统中,“AL0\n123\““和’\x41L0\10\04923”‘实际上是一样的:0x41对应A,10对应换行符,49对应数字1,由于转义序列49后面紧邻数字,所以必须写成\049。

从Lua5.3开始,也可以使用转义序列\u{h…h}来声明UTF-8字符,花括号中可以支持任意有效的十六进制。

4.2 长字符串/多行字符串

像长注释/多行注释一样,可以使用一对双方括号来声明长字符串/多行字符串。被方括号括起来的内容可以包括很多行,并且内容中的转义序列不会被转义。此外,如果多行字符串中的第一个字符是换行符,那么这个换行符会被忽略。在声明包含打断代码中的字符串时非常方便。

有时字符串中可能有类似a = b[c[i]]这样的内容,或者,字符串中可能有被注释掉的代码。为了应对这种情况,可以在两个左方括号之间加入任意数量的等号,如[===[。这样,只有在遇到包含相同数量等号的右方括号时才会结束。这个机制对于注释同样有效。

虽然Lua语言中的字符串常量可以包含任意字节,但是滥用也可能出现异常。同时,像”\r\n"一样的EOF序列在被读取时可能会被归一化为”\n"。作为替代方案,最好把这些可能引起歧义的二进制数据用十进制或十六进制的数值转义序列表示。从Lua5.2开始引入了转义序列\z,该转义字符会跳过其后的所有空白字符,直到遇到第一个非空白字符。

data = "\x00\x01\x02\x03\x04\x05\x06\x07\z
		\x08\x09\x0A\x0B\x0C\x0D\x0E\x0F"

实际上,\x08是紧邻着\x07的。

4.3 强制类型转换

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

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

当在数值后紧接着使用字符串连接时,必须使用空格将它们分开,否则Lua语言会把第一个点当作小数点。

由于字符串不是整型值,所以任何字符串参与的算术运算都会被当作浮点运算处理。

"10" + 1		--> 11.0

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

tonumber("  -3 ")		--> -3
tonumber(" 10e4 ")		--> 10000.0
tonumber("10e")			--> nil
tonumber("0x1.3p-4")		--> 0.07421875

默认情况下,函数tonumber使用地时十进制,但是也可以使用二进制到三十六进制之间地任意进制:

tonumber("100101", 2)		--> 37
tonumber("fff", 16)		--> 4095
tonumber("-ZZ", 36)		--> -1295
tonumber("987", 8)		--> nil

调用函数tostring可以将数值转换成字符串:

print(tostring(10) == "10")		--> true

这种转换总是有效,但是并不能控制输出字符串的格式(例如,结果中十进制数字的个数)。下一节中可以看到,通过函数string.format来控制。

与算数操作不同,比较操作符不会对操作数进行强制类型转换。此外,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与之相反。

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代表字符串的最后一个字符。这样,string.sub(s, 1, j)得到的是字符串s中长度为j的前缀,string.sub(s, i, -1)得到的是字符串s中第i个字符开始的后缀,string.sub(s, 2, -2)则是去掉第一个和最后一个字符的结果。

函数不会改变原有字符串的值,它只会返回一个新字符串。如果需要修改原字符串,那么必须把新的值赋值给它:

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

函数string.char和string.byte用于转换字符及其内部数值表示。

函数string.char接受零个或多个整数作为参数,然后将每个整数转换成对应的字符,最后返回由这些字符连接而成的字符串。

函数string.byte(s, i)返回字符串s中第i个字符的内部数值表示,该函数的第二个参数是可选的。strintg.byte(s)返回字符串s中第一个字符的内部数值表示。

print(string.char(97))				--> a
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

调用string.byte(s, i, j)返回索引i到索引j之间(包括i和j)的所有字符的数值表示:

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)		--> <h1>a title</h1>

在百分号和字母之间可以控制格式细节的其他选项。例如,%.4f表示小数点后保留四位小数;%02d表示一个十进制数至少由两个数字组成,不足两个的由0补齐,%2d表示用空格补齐(完整描述可以阅读C语言printf的相关文档)。

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

字符串标准库还包括了几个基于模式匹配的函数。

函数string.find用于在指定的字符串中进行模式搜索:

string.find("hello world", "wor")		--> 7  9
string.find("hello world", "war")		--> nil

如果找到了,返回匹配的开始和结束位置,否则返回nil。

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

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

该函数还会在第二个返回值中返回发生替换的次数。

4.5 Unicode编码

Lua5.3开始,Lua引入了一个用于操作UTF-8编码的Unicode字符串的标准库。

UTF-8是Web环境中用于Unicode的主要编码之一。由于UTF-8编码与ASCII编码部分兼容,所有UTF-8对于Lua也是一种理想的编码方式。这种兼容性保证用于ASCII的一些字符串操作技巧无需修改就可以用于UTF-8。

UTF-8使用变长的多个字节来编码一个Unicode字符。使用一个字节表示所有ASCII范围内的字符(小于128)。

由于Lua使用8个字节来编码字符,所以可以像操作其他字符串一样读写和存储UTF-8字符串。字符串常量也可以包含UTF-8数据(需要使用支持UTF-8编码的编辑器来处理使用UTF-8编码的源文件)。字符串连接对UTF-8字符串同样使用。对于字符串的比较会按照Unicode编码中的字符代码顺序进行。

Lua语言的操作系统库和输入输出库是否支持UTF-8取决于对应的操作系统。例如,Linux支持UTF-8,而Windows使用UTF-16(要在Windows处理Unicode,要么使用额外的库,要么修改Lua语言标准库)。

函数reverse、upper、lower、byte、char不适用于UTF-8字符串,因为他们针对的都是一字节字符。函数format和rep适用于UTF-8字符串(格式选项%c除外)。函数len和sub可以用于UTF-8字符串,其中的索引以字节为单位而不是以字符为单位。

现在来看一下新的utf8标准库。函数utf8.len返回指定字符串中UTF-8字符(代码点(一个Unicode中的字符可能要使用两个或以上的字节表示,一个完整的Unicode字符就叫做代码点))的个数。此外,还会验证字符串,如果发现包含无效的字节序列,则返回nil外加第一个无效字节的位置。

函数utf8.char和utf8.codepoint在UTF-8环境下等价于string.char和string.byte。

utf8中大多数函数使用字节为索引。例如,调用string.codepoint(s, i, j)时,i和j都会被当作字符串s中的字节位置。如果想使用字符位置作为索引,那么可以通过函数utf8.offset把字符位置转换为字节位置:

utf8.codepoint(s, utf8.offset(s, 5))

这样就获取到第五个字符的字节索引,然后作为参数调用函数codepoint。

utf8. offset的索引值可以时负值,代表从字符串末尾开始计数。

utf8标准库中的最后一个函数时utf8.codes,该函数用于遍历UTF-8字符串中的每一个字符:

for i, c in utf8.codes("Ação") do
    print(i, c)
end
	--> 1	65
	--> 2	231
	--> 4	227
	--> 6   111

改代码会遍历指定字符串中的所有字符,将每个字符对应的字节索引和编码赋给两个局部变量。