在 Lua 5.2 及之前版本,所有数值都以双精度浮点格式表示。从 Lua 5.3 开始,Lua 语言为数值提供了两种选择:
- 被称为 integer 的 64 位整型
- 被称为 float 的双精度浮点类型
整型的引入是 Lua5.3 的一个重要标志。在精简 Lua 模式下(small lua),该模式使用 32 位整型和单精度浮点类型。
这篇文章将学习 Lua 的数值(number) 和字符串(string)类型。
数值
数值常量
可以使用科学计数法(一个可选的十进制部分外加一个可选的十进制指数部分)书写数值常量。具有十进制小数或指数的数值会被当做浮点型值,否则会被当做整型值。
整型值和浮点型值的类型都是 number
,它们之间可以相互转换。而且,具有相同算术值的整型值和浮点型值在 Lua 语言中都是相同的:
1 | > type(1) |
如果的确需要区分整型值和浮点型值时,可以使用函数 math.type
:
1 | > math.type(1) |
1 | > math.type(1.00) |
Lua 也支持以 0x 开头的十六进制常量,而且支持十六进制的浮点数,这种十六进制浮点数由小数部分和以 p 或 P 开头的指数部分组成。在函数 string.format
中可以使用 %a
参数进行该格式的格式化输出。这种格式可以保存所有浮点数的精度。而且比十进制转换速度更快。
1 | > 0xff |
算术运算
对于 Lua5.3 引入的整型,主要建议是 开发人员要么选择忽略整型和浮点型二者之间的不同,要么就完整地控制每一个数值的表示
。因此,所有的算术运算符不论操作整型还是浮点型值,结果应该都是一样的。
对于算术运算符 +、-、*、/、//(整除)、%(取模)、^(幂等)
,如果两个操作数都是整型,那么结果也是整型值,否则就是浮点型值。但是对于除法和幂等运算不遵循上述规则,为了避免两个整型值相除(不一定能整除)和两个浮点型值相除导致不一样的结果,除法运算操作操作的永远是浮点数且产生浮点型结果。
1 | > 2 + 4 |
和除法一样,幂等运算的操作数也永远是浮点型,例如 2^-2
其实就是执行除法运算。
1 | > 2 ^ -2 |
整除(floor)//
会将得到的商向负无穷取整,从而保证结果是一个整数。因此它就和其他算术运算符遵循同样的规则:如果操作数是整型,那么结果就是整型,否则就是浮点型值(其值是一个整数)。
1 | > 3 // 2 |
取模运算的定义为 a % b == a - ((a // b) * b)
。对于整型操作数而言,取模运算结果的符号永远与第二个操作数的符号保持一致。对于实数类型的操作数而言,取模运算则有些不同,x - x % 0.01
恰好是 x 保留两位小数的结果,x - x % 0.001
恰好是 x 保留三位小数的结果。
1 | > x = 3.1415927 |
关系运算符
Lua 提供了以下关系运算符:<、>、<=、>=、==、~=
,这些关系运算符的结果都是 Boolean 类型:
- 相等性测试(==)、不等性测试(~=)运算符可以应用于任意两个值,当两个值的类型不同时,它们是不相等的,否则会根据它们的类型再对两者进行比较
- 比较数值时永远忽略数值的子类型,数值以整型还是浮点类型表示都无区别,只与算术值有关
1 | > 1 == nil |
数学库
Lua 提供了标准数学库 math,由一组标准的数学函数组成:包括取整函数、指数函数、最大/最小函数、三角函数等,同时也提供了常量 pi、huge(最大可表示数值,在大多数平台上代表 inf)。
函数 math.random 用于生成伪随机数,当不带参数调用时,返回一个
[0,1)
范围内均匀分布的伪随机实数。当使用整型值 n 为参数调用时,返回一个在范围[1, n)
范围内的位随机整数。当使用两个整型值l
和u
为参数调用时,返回在[l, u]
范围内的伪随机整数函数 randomseed 用于设置伪随机数发生器的种子。程序启动时,系统固定使用
1
作为种子初始化伪随机数发生器,这样程序每次运行时都会生成相同的伪随机数序列。可以使用math.randomseed(os.time())
来使用当前系统时间作为种子初始化随机数发生器floor
用于向负无穷取整、ceil
用于向正无穷取整、modf
用于向 0 取整(除了返回取整后的值以外,函数 modf 还会返回小数部分作为第二个结果)。如果想将数值 x 向最近的整数取整,可以使用floor(x+0.5)
的方式
表示范围
标准 Lua 使用 64 位来存储整型,其最大值为 2**63 - 1
。精简 Lua 使用 32 个比特位来存储整型值。数学库中定义了整型的最大值 math.maxinteger
和最小值 math.mininteger
。
对于浮点数而言,标准 Lua 使用双精度。标准 Lua 使用 64 个比特位来表示所有数值,其中 11 位为指数,能表示的范围从 -10**308 ~ 10**308
。双精度浮点数对于大多数应用而言是足够大的。
浮点型的值可以表示很大的范围,但是浮点型能够精确表示的整数范围为 [-2**53,2**53]
之间,在这个范围内,基本可以忽略整型和浮点型的区别,超出该范围之后,则应该谨慎地思考所使用的方式。
惯例
- 可以简单地通过增加
0.0
的方法将整型值强制转换为浮点型值。绝对值小于2**53
的所有整型值的表示与双精度浮点型值的表示方法一样。当绝对值超过了这个值的整型值,其在强制转换为浮点型值时可能导致精度损失 - 通过与零进行按位或运算,可以把浮点型值强制转换为整型值。在将浮点型值强制转换为整型值时,Lua 语言会检查数值是否与整型值表示完全一致,即:没有小数部分且其值在整型值的表示范围内,不满足条件则抛出异常
- 对小数进行取整必须显式地调用取整函数
- 另一种将数值强制转换为整型值的方式是使用
math.tointeger
,当输入参数无法转换为整型值时返回 nil
1 | > 9007199254740991 + 0.0 == 9007199254740991 |
运算符优先级
Lua 中运算符优先级如下:
- ^
- 一元运算符(-、#、~、not)
- / // %
- ..
- << >>
- &
- ~
- |
- < > <= >= ~= ==
- and
- or
在二元运算符中,除了幂运算和连接操作符是右结合之外,其他运算符都是左结合的。当不能确定某些表达式的运算符优先级时,应该显式地用括号来指定所希望的运算次序。
兼容性
- Lua5.2 支持的最大整数为
2**53
,而 Lua5.3 支持的最大整数为2**63
。 - Lua5.2 将所有的整数值格式化为整型,而 Lua5.3 则将所有浮点数格式化为浮点型(带有十进制小数或者指数)。因此在 Lua5.2 中
3.0
格式化为3
,而 Lua5.3 中3.0
格式化为3.0
输出
字符串
字符串用于表示文本,Lua 语言中的字符串是一串字节组成的序列。Lua 核心并不关心这些字节以何种方式编码文本:
- Lua 语言中的字符串可以存储包括空字符在内的所有数值代码,因此可以在字符串中存储任意的二进制数据
- 可以使用任意一种编码方法(UTF-8、UTF-16)来存储 Unicode 字符
Lua 中的字符串是不可变值,只能通过创建一个新的字符串的方式来达到修改目的。Lua 中的字符串也是自动内存管理的对象之一,即 Lua 语言会负责字符串内存的分配和释放。
- 可以使用长度操作符(
#
)来获取字符串的长度,该操作符返回字符串占用的字节数 - 可以使用连接操作符(
..
)来进行字符串连接,如果操作数中存在数值,会先将数值转换成字符串
1 | > a = "hello" |
字符串常量
使用一对双引号或者单引号来声明字符串常量。单引号、双引号两者是等价的,区别在于转义的时机:使用双引号声明的字符串中出现单引号时,单引号可以不用转义。使用单引号声明的字符串中出现双引号时,双引号可以不用转义。
Lua 字符串支持 C 语言风格的转义字符,例如 \n
等。也可以通过转义序列 \ddd
和 \xhh
来声明字符,其中 ddd
是由最多 3 个十进制数字组成的序列,hh
是由两个且必须是两个的十六进制数字组成的序列。
从 Lua5.3 开始,也支持使用转义序列 \u{h..h}
来声明 UTF-8 字符,花括号中可以支持任意有效的十六进制。
长字符串/多行字符串
可以使用一对 双括号
来声明长字符串/多行字符串常量,此时内容中的转义序列不会被转义。另外如果多行字符串的第一个字符是换行符,该换行符会被忽略。
当内容中本身含有双括号时,可以在两个左方括号之间加上任意数量的等号,例如 [===[
,这样字符串常量只有在遇到了包含相同数量等号的两个右方括号时才会结束(本例中即 ]===]
。Lua 语法扫描器会忽略所含等号数量不相同的方括号。通过选择恰当数量的等号,就可以无需修改原字符串的情况下声明任意的字符串常量。对于注释,这种机制同样有效,例如使用 --[=[
和 ]=]
来进行长注释,从而降低对内部已经包含注释的代码进行注释的难度。
从 Lua5.2 开始引入了转义序列 \z
,该转义符会跳过其后的所有空白字符,直到遇到第一个非空白字符。
强制类型转换
Lua 语言运行时提供了数值与字符串之间的自动转换:
- 当需要数值的地方出现了字符串,Lua 会自动尝试将字符串转换为数值
- 当需要字符串的地方出现了数值,Lua 会自动尝试将数值转换为字符串
1 | > print(10 .. 20) |
注意,当数值之后紧接着使用字符串连接时,必须使用空格将它们分开,否则 Lua 语言会将第一个点当成小数点。
使用 tonumber
可以显式地将一个字符串转换成数值。默认该函数使用十进制,但也可以指定二进制到三十六进制之间的任意进制。使用 tostring
可以将数值转换为字符串。
1 | > tonumber(" -3") |
注意,比较操作符不会对操作数进行强制类型转换。当比较运算符中混用了字符串和数值时,Lua 会抛出异常。
1 | > 2 < "2" |
字符串标准库
Lua 语言解释器本身处理字符串的能力有限。Lua 语言处理字符串的完整能力来自其字符串标准库:
- 函数
string.len(s)
返回字符串 s 的长度,等价于#s
string.rep(s, n)
返回字符串 s 重复 n 次的结果string.reverse()
用于字符串翻转string.lower(s)
返回字符串 s 的小写版本string.uppper(s)
返回字符串 s 的大写版本string.sub(s, i, j)
从字符串 s 中提取第 i 个到第 j 个字符(包括 i 和 j,字符串第一个字符索引为 1)。也支持负数索引,从结尾开始计数:-1 代表字符串的最后一个字符、-2 代表倒数第二个字符,依此类推string.char 和 string.byte
用于转换字符及其内部数值表示。string.char
接收0或多个整数作为参数,然后将每个整数转换为对应的字符,最后返回这些字符连接而成的字符串。string.byte(s, i)
返回字符串 s 中的第 i 个字符的内部数值表示(如果没有指定 i,则默认为第一个。i 也支持负数索引)。string.byte(s, i, j)
则返回索引 i 到 j 之间的所有字符的数值表示。例如{string.byte(s, 1, -1)}
会创建一个由字符串 s 中的所有字符代码组成的表string.format
用于进行字符串格式化,它可以将数值输出位字符串。格式化字符串中的指示符与 C 中 printf 函数的规则类似(其实 Lua 就是通过调用 C 语言标准库来完成这项工作的)string.find
用于在指定的字符串中进行模式搜索。如果找到了匹配的模式,则返回模式的开始和结束位置,否则返回 nil。string.gsub
则把所有匹配的模式用另一个字符串替换,其还会在第二个返回值中返回发生替换的次数
1 | > string.lower("Hello") |
也可以可以使用冒号操作符像调用字符串的一个方法那样来调用字符串标准库中的所有函数:
1 | > s = "ABC" |
Unicode 编码
UTF-8 使用变长的多个字节编码一个 Unicode 字符。首先字符串标准库中的函数 reverse、upper、lower、byte、char
不适用于 UTF-8 字符串,因为他们针对的都是一字节字符。format、rep
适用于 UTF-8 字符串(格式选项 %c
除外,该格式选项针对一字节字符)。len
、sub
可以用于 UTF-8 字符串,其中的索引以字节为单位而不是以字符为单位。
从 Lua5.3 开始,Lua 引入了用于操作 UTF-8 编码的 Unicode 字符串的标准库:
utf8.len
返回指定字符串中 UTF-8 字符的个数。此外该函数还会验证字符串,如果该函数发现字符串中包含无效的字节序列,则返回 false 外加第一个无效字节的位置。- 函数
utf8.char
和utf8.codepoint
是 UTF-8 环境中的string.char
和string.byte
。此时索引仍然是以字节为单位,如果想以字符位置作为索引,可以通过函数utf8.offset
把字符位置转换为字节位置 utf8.codes
用于遍历 UTF-8 字符串中的每一个字符,并将每个字符对应的字节索引和编码赋给两个局部变量
1 | > utf8.len('中国') |
Lua 并没有提供其他 Unicode 字符相关的特性,因为 Unicode 本身的复杂性,想要支持完整地 Unicode 需要巨大的表,而这与 Lua 精简的大小相矛盾。因此对于这些特殊需求来说,最好就是选择外部库。