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的整数都是实际感知意义上的,而不是表示形式上的)