3.1 数值常量

我们可以使用科学技术法(一个可选的十进制部分外加一个可选的十进制指数部分)书写数值常量,例如:

4.77e-3		--> 0.00477
0.3e12		--> 300000000000.0
4e+3		--> 4000.0
5E+20		--> 5e+020

具有十进制小数或者指数的数值会被当做浮点类型值,否则会被当作整型值。

由于整型值和浮点型值的类型都是"number",所以它们是可以相互转化的。同时,具有相同算术值的整型值和浮点类型值在Lua语言中是相等的:

1 == 1.0		--> true
-3 == -3.0		--> true
0.2e3 == 200		--> true

在少数情况下,需要区分整型值和浮点型值时,可以使用math.type:

math.type(3)		--> integer
math.type(3.0)		--> float

Lua语言也支持以0x开头的十六进制常量。与其它语言不同的是,Lua还支持十六进制的浮点数,这种十六进制浮点数以p或P开头的指数部分组成。

0xff		--> 255
0x1A3		--> 419
0x0.2		--> 0.125
0x1p-1		--> 0.5(p后面的数字表示2的幂次)
0xa.bp2		--> 42.75

可以使用%a参数,通过函数string.format最这种格式进行格式化输出:

string.format("%a",419)		--> 0x1.a3p+8
string.format("%a",0.1)		--> 0x1.999999999999ap-4

虽然难以阅读,但是可以保留所有浮点数的精度,并且比十进制的转换速度更快。

3.2 算术运算

除了加、减、乘、除、取负数(单目减法)等,还支持取整除法(floor除法),取模和指数运算。

当操作数一个是整型值一个是浮点型值时,Lua语言会在进行算术运算前先将整型值转换为浮点型值:

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

由于两个整数相除的结果不一定时整除,因此触发不遵循整型值和整型值进行算术运算的结果依然是整型值,为了避免两个整型值相除和两个浮点型值相除导致不一样的结果,除法运算操作的永远是浮点数且产生浮点型值的结果:

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

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

3 // 2		--> 1
3.0 // 2	--> 1.0
4 // 2		--> 2
-9 // 2		--> -5

以下公式是取模运算的定义:

a % b = a - ((a // b) * b)

对于实数类型的操作数,取模运算有一些不同点。例如,x-x%0.01恰好是x保留两位小数的结果,x-x%0.001恰好是x保留三位小数的结果:

x = math.pi
x - x % 0.01	--> 3.14
x - x % 0.001	--> 3.141

表达式angle % (2 * math.pi)实现将任意弧度归一化到(0,2π)

Lua语言同样支持幂运算,使用符号^表示。像除法一样,幂运算的操作数也永远是浮点型值。

2^2	--> 4.0
4^0.5	--> 2.0
8^0.5   --> 2.8284271247462

3.3 关系运算

Lua语言提供了下列关系运算:

<      >      <=      >=      ==      ~=

这些关系运算的结果都是Boolean类型

==用于相等性测试,~=用于不等性测试。这两个运算符可以用于任意两个值,当这两个值类型不同时,Lua语言认为它们不相等;否则,会根据他们的类型再对两者进行比较。

比较数值时应永远忽略数值的子类型,只与算数值有关(尽管如此,比较具有相同子类型的数值时效率更高)。

3.4 数学库

Lua语言提供了标准数学库math。标准数学库由一组标准的数学函数组成,包括三角函数(sin、cos、tan、asin等)、指数函数、取整函数、最大和最小函数max和min、用于生成伪随机数的为随机函数(random)以及常量pi和huge(最大可表示值,在大多数平台上代表inf)。

所有的三角函数都以弧度为单位,并通过函数deg和rad进行角度和弧度的转换。

3.4.1 随机数发生器

函数math.random用于生成伪随机数,共有三种调用方式。当不带参数调用时,它返回一个在[0,1)范围内均匀分布的伪随机实数。当使用带有一个整型值n的参数调用时,它返回一个[1,n]范围内的伪随机整数。当使用带有两个整型值l和u的参数时,返回在[l,u]范围内的伪随机整数。

函数randomseed用于设置伪随机数发生器的种子。该函数的唯一参数是数值类型的种子。一个程序启动时,系统固定使用1为种子。如果不设置其他种子,那么每次程序都会生成相同的伪随机数序列。方便调试,但是会导致游戏生成重复内容,为了解决这个问题,通常调用math.randomseed(os.time())来使用当前系统时间作为种子初始化(os.time会在后续介绍)。

3.4.2 取整函数

提供了三个取整函数:floor、ceil和modf。其中,floor向负无穷取整,ceil向正无穷取整,modf向零取整。当取整结果能用整型表示时,返回结果为整型值,否则返回浮点型值(表示的是整型值)。除了返回取整后的值以外,函数modf还会返回小数部分作为第二个结果。

math.floor(3.3)		--> 3
math.floor(-3.3)	--> -4
math.ceil(3.3)		--> 4
math.ceil(-3.3)		--> -3
math.modf(3.3)		--> 3	0.3
math.modf(-3.3)		--> -3	-0.3
math.floor(2^70)	--> 1.1805916207174e+21

如果想将数值x向最近的整数取整,可以对x+0.5调用floor函数。不过当x很大时,可能会导致错误。

x = 2^52 + 1
print(string.format("%d %d", x, math.floor(x + 0.5)))
	--> 4503599627370497 4503599627370498

因为2^52 + 1.5的浮点值表示是不精确的,因此内部会以我们不可控的方式取整。为了避免这个问题,可以单独地处理数值:

function round (x)
	local f = math.floor(x)
	if x == f then return f
	else return math.floor(x + 0.5)
	end
end

上述代码其实是达到了一个四舍五入后返回一个整数的效果,但是如果我们想对半个整数进行无偏取整(unbiased rounding),即向距离最近的偶数取整半个整数,在x + 0.5是奇数的情况下不成立:

math.floor(3.5 + 0.5)		--> 4
math.floor(2.5 + 0.5)		--> 3(期待2)

可以修改为:

function round (x)
	local f = math.floor(x)
	if (x == f) or (x % 2.0 == 0.5) then 
        return f
	else 
        return math.floor(x + 0.5)
	end
end

3.5 表示范围

标准Lua使用64个比特位来存储整型值,其最大值为263 - 1,约等于1019;精简Lua使用32个比特位来存储整型值,其最大值约为20亿。数学库中的常量定义了整型值的最大值(math.maxinteger)和最小值(math.mininteger)。

以下行为对所有涉及整型值的算术运算都是一致且可预测的:

math.maxinteger + 1 == math.mininteger		--> true
math.mininteger - 1 == math.maxinteger		--> true
-math.mininteger == math.mininteger		--> true
math.mininteger // -1 == math.mininteger	--> true

对于浮点数,标准Lua使用双精度64个比特位表示,其中11位为指数;精简Lua使用32个比特位来表示单精度浮点数,有8位为指数。双精度对于大多数实际应用是足够大的,但是存在精度限制。

由于整型值和浮点型值的表示范围不同,当超过他们的表示范围时会产生不同的结果:

math.maxinteger + 2		--> -9223372036854775807
math.maxinteger + 2.0		--> 9.2233720368548e+18

第一行由于整型值发生溢出导致计算结果错误,第二行由于浮点数精度有限导致取近似值,可以通过如下比较运算证明:

math.maxinteger + 2.0 == math.maxinteger + 1.0	--> true

只有浮点型才能表示小数。浮点型值可以表示很大的范围,但是浮点型值能够表示的整数范围被精确地限制在[-253,253]之间。由于包含52位尾数+隐含的1位前导位,共53位二进制有效位。因此,小于或等于253的整数可以被精确表示,但大于253的整数无法精确表示。在这个范围内可以忽略整型和浮点型的区别,超过则应该谨慎地思考。

3.6 惯例

我们可以简单地通过加0.0的方式将整形强制转换为浮点型值,一个整型值总是可以被转换成浮点型值。但是对于不在[-253,253]区间内的整型值在转换成浮点型值时可能导致精度损失。

通过与零进行按位或运算,可以把浮点型值强制转化为整型值:

2^53			--> 9.007199254741e+015
2^53 | 0		--> 9007199254740992

在将浮点型值强制转化为整型值时,Lua语言会检查数值是否与整型值表示完全一致,即没有小数部分以及其值在整型值的表示范围内,若不满足条件会抛出异常:

3.2 | 0		--> stdin:1: number has no integer representation
2^64 | 0	--> stdin:1: number has no integer representation
math.random(1, 3.5)
-->stdin:1: bad argument #2 to 'random' (number has no integer representation)

另一种把数值强制转换为整型值的方式是使用函数math.tointeger,该函数会在输入参数无法转换为整型值时返回nil:

math.tointeger(-258.0)		--> -258
math.tointeger(2^30)		--> 1073741824
math.tointeger(5.01)		--> nil
math.tointeger(2^64)		--> nil

这个函数在需要检查一个数字能否被转换成整型值时有位有用:

function cond2int (x)
	return math.tointeger(x) or x
end

3.7 运算符优先级

Lua语言中的运算符优先级(优先级从高到低):

^

一元运算符(-      #      ~      not)

*      /      //      %

+      -

..                    (连接)

«      »             (按位移位)

&                    (按位与)

~                    (按位异或)

|                     (按位或)

<      >      <=      >=      ~=      ==

and

or

在二元运算符中,除了幂运算和连接操作符是右结合的外,其他运算符都是左结合的。因此,一下各个表达式左右等价:

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)

当不能确定优先级时,应该显示地用括号来指定所希望的运算次序。

3.8 兼容性

Lua5.3引入的整型值导致了一定的不兼容。Lua5.3和Lua5.2之间的最大不同就是整数的表示范围。Lua5.2支持的最大整数为253,而Lua5.3支持的最大整数为263-1。

与整型引入相关的问题的根源在于,Lua语言将数值转换为字符串的方式。Lua5.2将所有整数值格式化为整型(不带小数点),而Lua5.3则将所有的浮点数格式化为浮点型。因此Lua5.2会将3.0格式化为"3"输出,而Lua5.3会将其格式化为"3.0"输出。

(此处说明,Lua5.2是没有专门表示整型的,它只有一种数字类型,即双精度浮点数类型。所以上述提到的相关的5.2的整数都是实际感知意义上的,而不是表示形式上的)