Lua数值类型详解:浮点数、整数与算术运算 | 南锋

南锋

南奔万里空,脱死锋镝余

Lua数值类型详解:浮点数、整数与算术运算

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

1
2
3
4
5
> 4             --4
> 0.4 --0.4
> 4.57e-3 --0.00457
> 0.3e12 --3.0000000000.0
> 5E+20 --5e+20

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

1
2
3
>type(3)			--number
>type(3.5) --number
>type(3.0) --number

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

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

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

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

在Lua5.3中:

1
2
3
4
>3			--3
>3.0 --3.0
>1000 --1000
>1e3 --1000.0

Lua语言还像其他语言一样也支持0x开头的十六进制常量。与其他很多变成语言不通,Lua语言还支持十六进制的浮点数,这种十六进制浮点数部分由小数部分和以p或P开头的指数部分组成。例如:

1
2
3
4
5
>0xff		--255
>0x1A3 --419
>0x0.2 --0.125
>0x1p-1 --0.5
>0x.bp2 --42.75

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

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

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

算术运算

除了加减乘除、取负数等常见的算术运算外,Lua语言还支持取整除法、取模和指数运算。
两个整数型值进行加减乘除和取负操作的结果仍然是整型值。对于这些算术运算而言,操作数是用整型还暗示浮点型表示的整数都没有区别:

1
2
>13 + 15 		--28
>13.0 + 15.0 --28.0

如果两个操作数都是整型值,那么结果也是整型值;否则,结果就是浮点型值。当操作数一个是整型值一个是浮点型值时,Lua语言会在进行算术运算前将整型值转换为浮点型值:

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

由于两个整数相除的结果并不一定是整数,因此除法不遵循上述规则。为了避免两个整型值相除和两个浮点型值相除导致不一样的结果,除法运算操作永远是浮点数且产生浮点型值的结果:

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

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

1
2
3
4
5
6
>3 // 2			--1
>3.0 // 2 --1.0
>6 // 2 --3
>6.0 // 2.0 --3.0
>-9 // 2 --5
>1.5 // 0.5 --3.0

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

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

如果操作数是整数,那么取模运算的结果也是整数。因此,取模运算也遵从与算术运算相同的规律,即如果两个操作数均是整型值,则结果为整型,否则为浮点型。
对于整型操作数而言,取模运算的含义没有什么特别的,其结果的符号永远与第二个操作数的符号保持一致。特别地,对于任意指定的正常数K,即使x是负数,表达式x%K的结果也永远在[0,K-1]之间。例如,对于任意整数值i,表达式i%2的结果均是0或1。
对于实数类型的操作数而言,取模运算有一些不同。例如,x-x%0.01恰好是x保留两位小数的结果,x-x%0.001恰好是x保留三位小数的结果:

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

再比如,我们可以使用取模运算检查某辆车在拐过了指定的角度后是否能够原路返回。假设使用度作为角度的单位,那么我们可以使用如下的公式:

1
2
3
4
5
local tolerance = 10
function isturnback( angle)
angle = angle % 360
return (math.abs(angle - 180) < tolerance)
end

该函数对负的角度而言也同样适用:

1
print(istrunback(-180))			--turn

假设使用弧度作为角度的单位,那么我们只需要简单地修改常量的定义即可:

1
2
3
4
5
local tolerance = 0.17
function isturnback( angle)
angle = angle % (2 * math.pi)
return (math.abs(angle - 180) < tolerance)
end

表达式angle%(2*math.pi)实现了将任意范围的角度归一化到[0,2pi]之间。

Lua表达式同意支持幂运算,使用符号^表示,像除法一样,幂运算的操作数也永远是浮点类型(整型值在幂运算时不能整除,例如,$2^{-2}$的结果不是整型数)。我们可以使用$x^{0.5}$来计算x的平方根,使用$x^{1/3}$来计算x的立方根。

关系运算

Lua语言提供了下列关系运算:
<,>,<=,>=,==,~=
这些关系运算的结果都是Boolean类型。
==用于相等性测试,~=用于不等性测试。这两个运算符可以应用于任意两个值,当这两个值的类型不同时,Lua语言认为它们是不相等的;否则,会根据它们的类型再对两者进行比较。
比较数值时应用户忽略数值的子类型,数值究竟是以整型还是浮点型类型表示并无区别,只娱算术有关。

数学库

Lua语言提供了标准数学库math。标准数学库由一组标准的数学函数组成,包括三角函数、指数函数、取证函数、最大和最小函数、用于生成伪随机数函数(random)以及常量pi和huge。
详情可见链接:https://blog.csdn.net/Silent_F/article/details/86547290
所有的三角函数都以弧度为单位,并通过函数deg和rad进行角度和弧度的转换。

随机数发生器

函数math.random用于生成随机数,共有三种调用方式。当不带参数调用时,该函数将返回一个在[0,1)范围内均匀分布的随机实数。当使用带有一个整型值n的参数调用时,该函数将返回一个在[1,n]范围内的随机整数。例如,我们可以通过调用random(6)来模拟掷骰子的结果。当使用带有两个整数值l和u的参数调用时,该函数返回在[l,u]范围内的随机整数。
函数randomseed用于设置随机数发生器的种子,该函数的唯一参数就是数值类型的种子。在一个程序启动时,系统固定使用1为种子初始化随机数发生器。如果不设置其他的种子,那么每次程序运行时都会生成相同的随机数序列。从调试的角度看,这是一个不错的特行,然而,对于一个游戏来说却会导致相同的场景重复地出现。为了解决这个问题,通常调用math.randomsee(os.time())来使当前系统时间作为种子初始化随机数发生器。

取证函数

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

1
2
3
4
5
6
7
>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函数。不过,当参数是一个很大的整数时,简单的加法可能会导致错误。例如,考虑如下代码:

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

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

1
2
3
4
5
6
7
8
function round(x)
local f = math.floor(x)
if x == f then
return f
else
return math.floor(x + 0.5)
end
end

上例中的函数总是会向上取整半个整数。如果想进行无偏取整,即向距离最近的偶数取整半个整数,上述公式在x + 0.5是奇数的情况下产生不正确的结果:

1
2
>math.floor(3.5 + 0.5) 		--4 (ok)
>math.floor(2.5 + 0.5) --3 (wrong)

这时,还是可以利用取整操作来解决上面的问题:表达式(x%2.0 == 0.5)只有在x + 0.5为奇数时为真。基于这些情况,定义一个无偏取整函数就很简单了:

1
2
3
4
5
6
7
8
9
10
11
12
13
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

print(round(2.5)) -- 2
print(round(3.5)) -- 4
print(round(-2.5)) -- -2
print(round(-1.5)) -- -2

表示范围

大多数编程语言使用某些固定长度的比特位来表达数值。因此,数值的表示在范围和精度上都是有限制的。
标准Lua使用64个比特位来存储整型值,其最大值为$2^{63}-1$,约等于$10^{19}$;精简Lua使用32个比特位存储整型值,其最大值约为20亿。数学库中的常量定义了整型值的最大值(math.maxinteger)和最小值(math.mininteger)。
64位整型值中的最大值是一个很大的数值:全球财富总和(按美分计算)的数千倍和全球人口总数的数十亿倍。尽管这个数值很大,但是仍然有可能发生溢出。当我们在整型数操作时出现比mininteger更小或者比maxinteger更大的数值时,结果就会回环。
在数学领域,回环的意思是结果只能在mininteger和maxinteger之间,也就是对$2^{64}$取模的算术结果。在计算机领域,回环的意思是丢弃最高进位。假设最高进位存在,其将是第65个比特位,代表$2^{64}$。因此,忽略第65个比特位不会改变值对$2^{64}$取模的结果。在Lua语言中,这种行为对所有涉及整型值的算术运算都是一致且可预测的:

1
2
3
4
>math.maxinteger + 1 == math.mininteger			--ture
>math.mininteger - 1 == math.maxinteger --true
>-math.mininteger == math.mininteger --true
>math.mininteger // -1 == math.mininteger --true

最大可以表示的证书是0x7ff…fff,即除最高位(符号位,零为非负整数)外其余比特位均为1.当我们对0x7ff…fff加1时,其结果变为0x800…000,即最小可表示的整数。最小整数比最大整数的表示幅度大1:

1
2
3
4
>math.maxinteger		--9223372036854775807
>0x7fffffffffffffff --9223372036854775807
>math.maxinteger --9223372036854775808
>0x8000000000000000 --9223372036854775808

对于浮点数而言,标准Lua使用双精度。标准Lua使用64个比特位表示所有数值,其中11位为指数。双精度浮点数可以表示具有大致16个有效十进制位的数,范围从$-10^{308}$到$10^{308}$。精简Lua使用32个比特位表示的单精度浮点数,大致具有7个有效十进制位,范围从$-10^{38}$到$10^{38}$。
双精度浮点数对于大多数实际应用而言是足够大的,但是我们必须了解精度的限制。如果我们使用十位表示一个数,那么1/7会被取整到0.142857142。如果我们使用十位计算1/7 * 7,结果会是0.999999994而不是1。此外,用十进制表示的有限小数在用二进制表示时可能是无线小数。例如,12.7 -20 + 7.3即便是用双精度表示也不是0,这是由于12.7和7.3的二进制表示不是有限小数。
由于整型值和浮点型值的表示范围不同,因此当超过它们的表示范围时,整型值和浮点型值的算术运算会产生不同的结果:

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

在上例中,两个结果从数学的角度看都是错误的,而且它们错误的方式不同。第一行对最大可表示整数进行了整型求和,结果发生了回环。第二行对最大可表示整数进行了浮点型求和,结果被取整成了一个近似值,这可以通过如下的比较运算证明:

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

尽管每一种表示方法都有其优势,但是只有浮点型才能表示小数。浮点数的值可以表示很大的范围,但是浮点型能够表示的整数范围被精确地限制[$-2^{53}$,$2^{53}$]之间。在这个范围内,我们基本可以忽略整型和浮点型的区别;超出这个范围后,我们则应该谨慎地思考所使用的表示方式。

惯例

我们可以简单地通过增加0.0的方法将整型值强制转换为浮点型值,一个整型值总是可以被转换成浮点型值:

1
> -3 + 0.0 				-- -3.0

小于$2^{53}$的所有整型值的表示与双精度浮点型值的表示一样,对于绝对值超过了这个值的整型值而言,在将其强制转换为浮点型值时可能导致精度损失:

1
2
3
> 9007199254740991 + 0.0 == 90071992547440991		--true
> 9007199254740992 + 0.0 == 90071992547440992 --true
> 9007199254740993 + 0.0 == 90071992547440993 --false

在最后一行中,$2^{53} + 1$的结果被取整为$2^{53}$,打破了等式,表达式结果为false。
通过与零进行按位或运算,可以把浮点型值强制转换为整型值:

1
2
> 2^53				-- 9.007199254741e+15 (浮点型值)
> 2^53|0 -- 9007199254730992 (整型值)

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

1
2
3
4
5
6
> 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'(数值没有用整型表示)

对小数进行取整必须显示地调用取整函数。
另一种把数值强制转换为整型值的方式是使用函数math.tointeger,该函数会在输入参数无法转换为整型值时返回nil:

1
2
3
4
>math.tointeger(-258.0)			-- -258
>math.tointeger(2^30) -- 1073741824
>math.tointeger(5.01) -- nil (不是整数)
>math.tointeger(2^64) -- nil (超出范围)

这个函数在需要检查一个数字能否被转换为整型值时尤为有用。例如,以下函数在可能时会将输入参数转换为整型值,否则将保持原来的值不变:

1
2
3
function cond2int(x)
return math.tointerger(x) or x
end

运算符优先级

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

1
2
3
4
5
6
7
8
9
10
11
12
^
一元运算符(- # ~ not )
* / // %
+ -
..(连续)
<< >> (按位移位)
&(按位与)
~(按位异或)
|(按位或)
< > <= >= ~= ==
and
or

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

1
2
3
4
5
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)

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

+