Lua连续教程之Lua字符串

字符串用于文本。Lua语言中的字符串即可以表示单个字符,也可以表示一整本书籍。在Lua语言中,操作100K或者1M个字母组成的字符串的程序也很常见。

Lua语言中的字符串是不可变值。我们不能像在C语言中那样直接改变某个字符串中的某个字符,但是我们可以通过创建一个新的字符串的方式来达到修改的目的。例如:

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

像Lua语言中的其他对象一样,Lua语言中的字符串也是自动内存管理的对象之一。这意味着Lua语言会负责字符串的分配和释放,开发人员无须关注。
可以使用长度操作符(#)获取字符串的长度:
1
2
3
a = "hello"
print(#a) --5
print(#"good bye") --8

该操作符返回字符串占用的字节数,在某些编码中,这个值可能与字符串中字符的个数不同。
我们可以使用连接操作符..(两个点)来进行字符串连接。如果操作数中存在数值,那么Lua语言会先把数值转换成字符串:
1
2
> "Hello" .. "World" 		-- Hello World
> "result is " .. 3 -- result is 3

在某些语言中,字符串连接使用的是加号,但实际上3+5和3..5是不一样的。
应该注意,在Lua语言中,字符串是不可变量。字符串连接总是创建一个新字符串,而不会改变原来作为操作数的字符串:
1
2
3
> a = "Hello"
> a .. "World" -- Hello World
> a -- Hello

字符串常量

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

1
2
a = " a line "
b = ' another line'

使用双引号和单引号声明字符串是等价的。它们两者唯一的区别在于,使用双引号声明的字符串中出现单引号时,单引号可以不用转义;使用单引号声明的字符串中出现双引号时,双引号可以不用转义。
Lua语言中的字符串支持下列C语言风格的转义字符:
1
2
3
4
5
6
7
8
9
10
\a			响铃(bell)
\b 退格(back space)
\f 换页(form feed)
\n 换行(newline)
\r 回车(carriage return
\t 水平制表符(horizontal tab)
\v 垂直制表符(vertical tab)
\\ 反斜杠(backslash)
\" 双引号(double quote)
\' 单引号(single quote)

下述示例展示了转义字符的使用方法:
1
2
3
4
5
6
7
8
>print("one line \n next line \n\"in quotes\",'in quotes' ")
one line
next line
"in quotes" , 'in quotes'
>print('a backslash inside quotes:\' \\\ '')
a backslash inside quotes: '\'
>print("a simpler way: '\\' ")
a simpler way: '\'

在字符串中,还可以通过转义\add和\xhh来声明字符。其中,add是由最多3个十进制数字组成的序列,hh是由两个且必须是两个十六进制数字组成的序列。

长字符串/多行字符串

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

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

有时字符串中可能有类似a = b[c[i]]这样的内容,或者,字符串中可能有被注释掉的代码。为了应对这些情况,可以在两个左方括号之间加上任意数量的等号,如[===[。这样,字符串常量只有在遇到了包含了相同数量等号的两个右括号时才会结束。Lua语言的语法扫描器会忽略所含等号数量不相同的方括号。通过选择恰当数量的等号,就可以在无须修改原字符串的情况下声明任意的字符串常量了。
对注释而言,这种机制也同样有效。例如,我们可以使用—[=和]=]来进行长注释,从而降低了对内部已经包含注释的代码进行注释的难度。
当代码中需要使用常量文本时,使用长字符串是一种理想的选择。但是,对于非文本的常量我们不应该滥用长字符串。虽然Lua语言中的字符串常量可以包含任意字节,但是滥用这个特行并不明智。同时,像”\r\n”一样的EOF序列在被读取的时候可能会被归一化成”\n”。作为替代方案,最好就是把这些可能引起歧义的二进制数据用十进制或十六进制的数值转义系列进行表示,例如”\x13\x01\xA1\xBB”。不过,由于这种转义表示行程的字符串往往很长,所以对于长字符串来说仍可能是个问题。针对这种情况,从Lua5.2开始引入了转义序列\z,该转义符会跳过其后的所有空白字符,直到遇到第一个非空白字符。下列中演示了该转义符的使用方法:
1
2
data = "\x00\x01\x02\x03\x04\x05\x06\x07\z
\x08\x09\x0A\x0B\x0C\x0D\x0E\x0F"

第一行最后的\z会跳过其后的EOF和第二行的制表符,因此最终得到的字符串中,\x08实际上是紧跟着\x07的。

强制类型转换

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

1
print(10 .. 20 )  	--1020

当在数值后紧接着使用字符串连接时,必须使用空格将它们分开,否则Lua语言会把第一个点当成小数点。
很多人认为自动强制类型转换算不上是Lua语言中的一项好设计。作为原则之一,建议最好不要完全寄希望于自动强制类型转换。虽然在某些场景下这种机制很便利,但同时也给语言和适用这种机制的程序带来了复杂性。
作为这种”二类状态”的表现之一,Lua5.3没有实现强制类型转换娱整型的集成,而是采用了另一种更简单和快速的实现方式:算术运算的规则就是只有在两个操作数都是整型值时结果才是整型。因此,由于字符串不是整型值,所以任何有字符串参与的算术运算都会被当做浮点运算处理:
1
>"10" + 1          --11.0

如果需要显示地将一个字符串转换成数值,那么可以使用函数tonumber。当这个字符串的内容不能表示为有效数字时该函数返回nil;否则,该函数就按照Lua语法扫描器的规则返回对应的整型值或浮点类型值:
1
2
3
4
> tounmber(" -3 ")			-- -3
> tounmber(" 10e4 ") -- 100000.0
> tounmber(" 10e ") -- nil (not a valid number)
> tounmber(" 0x1.3p - 4") -- 0.07421875

默认情况下,函数tonumber使用的是十进制,但是也可以指明使用二进制到三十六进制之间的任意进制:
1
2
3
4
> tounmber("100101", 2)			-- 37
> tounmber("fff" ,16) -- 4095
> tounmber("-ZZ",36) -- -1295
> tounmber("987",8) -- nli

在最后一行,对于制定的进制而言,传入的字符串是一个无效值,因此函数tonumber返回nil。
调用函数tonumber可以将数值转换成字符串:
1
print(tostring(10) == "10")   --true

上述的这种转换总是有效,但我们需要记住,使用这种转换时并不能控制输出字符串的格式。

字符串标准库

Lua语言解释器本身处理字符串的功能是十分有限的。一个程序能够创建字符串、连接字符串、比较字符串和获取字符串的长度,但是,它并不能提取字符串的子串或检视字符串的内容。Lua语言处理字符串的完整能力来自其字符串标准库。
字符串标准库中的一些函数非常简单:函数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
> string.rep("abc",3)			-- 游戏开发abcabc
> string.reverse("A Long Line!") -- !eniL gnoL A
> string.lower("A Long Line!") -- a long line!
> string.upper("A Long Line!") -- A LONG LINE!

作为一种典型的应用,我们可以使用如下代码在忽略大小写差异的原则下比较两个字符串:
1
string.lower(a) < string.lower(b)

函数string.sub(s,i,j)从字符串s中提取第i个到第j个字符。该函数也支持负数索引,负数索引从字符串的结尾开始计数:索引-1代表字符串的最后一个字符,索引-2代表倒数第二个字符,依此类推。这样,对字符串s调用函数string.sub(s,1,j)得到的是字符串s中长度为j的前缀,调用string.sub(s,j,-1)得到的是字符串s从第j个字符开始的后缀,调用string.sub(s,2,-2)返回的是去掉字符串s中第一个和最后一个字符的结果。
请注意,Lua语言中的字符串是不可变的。和Lua语言中的所有其他函数一样,函数string.sub不会改变原有字符串的值,它只会返回一个新字符串。一种常见的误解是以为string.sub(s,2,-2)返回的是修改后的s。如果需要修改原字符串,那么必须把心的值赋值给它:
1
s = string.sub(s,2,-2)

函数string.char 和string.byte用于转换字符串及其内部数值表示。函数string.char接收零个或多个证书作为参数,然后将每个整数转换成对应的字符,最后返回由这些字符连接而成的字符串。函数string.byte(s,i)返回字符串s中第i 个字符的内部数值表示,该函数的第二个参数是可选的。调用string.byte(s)返回字符串s中第一个字符的内部数值表示。在下面例子中,假定字符串是用ASCII表示的:
1
2
3
4
5
print(string.char(97))			-- a
i = 99; print(string.char(i,i+7,i+2)) -- cde
print(string.byte("abc")) -- 97
pring(string.byte("abc" , 2)) -- 98
pring(string.byte("abc" , -1)) -- 99

在最后一行中,使用负数索引来访问字符串的最后一个字符。
调用string.byte(s,i,j)返回索引i到j之间的所有字符的数字表示:
1
print(string.byte("abc",1,2)     -- 97 98

一种常见的写法是{string.byte(s,1,-1)},该表达式会创建一个由字符串s中的所有字符代码组成的表。
函数string.format是用于进行字符串格式化和将数值输出为字符串的强大工具,该函数会返回一个参数的副本,其中的每一个指示符都会被替换为使用对应格式化后的对应参数。格式化字符串中的指示符与C语言中函数printf的规则类似,一个指示符由一个百分号和一个代表格式化方式的字母组成:d代表一个十进制整数、x代表一个十六进制整数、f代表一个浮点数、s代表字符串等等。
1
2
3
4
5
6
> 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.foramt("<%s>%s</%s>",tag,title,tag) -- <h1> a title</h1>

在百分号和字母之间可以包含用于控制格式细节的其他选项。例如,可以指定一个浮点数中小数的位数:
1
2
3
print(string.foramt("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

在上例中,%.4f表示小数点后保留4位小数;%02d表示一个十进制数至少由两个数字组成,不足两个数字的用0补齐,而%2d则表示用空格来补齐。关于这些指示符的完整描述可以参考C语言printf函数的相关文档。
可以使用冒号操作符像调用字符串的一个方法那样调用字符串中标准库中的所有函数。例如,string.sub(s,i,j)可以重写成s:sub(i,j),string.upper(s)可以重写成s:supper()。
字符串标准库还包括了几个基于模式匹配的函数。函数string.find用于在指定的字符串中进行模式搜索:
1
2
> string.find("hello world" , "wor")		-- 7 9
> string.find("hello world" , "war") -- nil

如果该函数在指定的字符串中找到了匹配的模式,则返回模式的开始和结束位置,否则返回nil。函数string.gsub(Global SUBstitution)则把所有匹配的模式用另一个字符串替换:
1
2
3
> 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

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

Unicode编码

UTF-8是Web环境中用于Unicode的主要编码之一。由于UTF-8编码娱ASCII编码部分兼容,所以UTF-8对于Lua来说是一种理想的编码方式。这种兼容性保证了用于ASCII字符串的一些字符操作技巧无须修改就可以用于UTF-8字符串。
UTF-8使用变长的多个字节来编码一个Unicode字符。例如,UTF-8编码使用一个字节的65来代表A,使用两个字节的215-144代表希伯来语字符Aleph。UTF-8使用一个字节表示所有ASCII范围内的字符(小于128)。对于其他字符,则使用字节序列表示,其中第一个字节的范围时[194,244],而后续的字节范围时[128,191]。更准确地说,对于两个字节组成的序列,第一个字节的范围是[194,223];对于三个字节组成的序列来说,第一个字节的范围是[224,239];对于四个字节组成的序列来说,第一个字节的范围是[240,224],这些范围互相之间均不重叠。这种特点保证了任意字符对应的字节序列不会在其他字符对应的字节序列中出现。特别地,一个小于128的字节永远不会在多字节序列中,它只会代表与之对应的ASCII字符。
Lua语言中的一些机制对UTF-8字符串来说同样“有效”。由于Lua语言使用8个字节来编码字符,所以可以像操作其他字符串一样读写和存储UTF-8字符串。字符串常量也可以包含UTF-8数据。字符串连接UTF-8字符串同样适用。对字符串比较会按照Unicode编码中的字符代码顺序进行。
Lua语言的操作系统库和输入输出库是与对应系统之间的主要接口,所以它们是否支持UTF-8取决于对应的操作系统。例如,在Linux操作系统下文件名要使用UTF-8编码,而在Windows操作系统下文件名使用UTF-16编码。因此,如果要在Windows操作系统中处理Unicode文件名,要么使用额外的库,要么就修改Lua语言的标准库。