数字
到 5.2 版为止,Lua 均使用双精度浮点格式,表示所有数字。从 5.3 版开始,Lua 使用了两种不同数字表示法: 64 位整数(简称 整数,integers)和双精度浮点数(简称 浮点数,floats)。(请注意,在本书中,“浮点数,float” 一词不意味着单精度,single precision。)对于受限平台,我们可以将 Lua 5.3 编译为 Small Lua,他会使用 32 位的整数,和单精度浮点数。注 1
整数的引入,是 Lua 5.3 的标志,也是其与之前版本 Lua 的主要区别。尽管如此,这一变化几乎没有造成兼容问题,因为双精度浮点数,可以精确表示 2^53 以内的整数。我们在此介绍的大部分内容,也适用于 Lua 5.2 以及更旧的版本。在本章最后,我将详细讨论兼容问题。
数值
Numerals
在书写数字常量时,咱们可以选择使用小数部分,以及小数指数,比如下面的例子:
> 4
4
> 0.4
0.4
> 4.57e-3
0.00457
> 0.3e12
300000000000.0
> 5E+20
5e+20
带小数点或指数的数字,会被视为浮点数,否则被视为整数。
整型值和浮点型值的类型,都是 "number"
:
> type(3)
number
> type(3.5)
number
> type(3e-3)
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 5.3 会以不同的方式,显示他们:
> 3
3
> 3.0
3.0
> 1000
1000
> 1e3
1000.0
与许多其他编程语言一样,Lua 也支持十六进制的常数,hexadecimal constants,只需在常数前加上 0x
。与许多其他编程语言不同,Lua 还支持浮点的十六进制常量,这种常量可以有小数部分,以及二进制的指数,前缀为 p
或 P
注 2。下面的示例,演示了这种数字格式:
> 0xff
255
> 0x1A3
419
> 0x0.2
0.125
> 0xa.bp2
42.75
> 0x1p-1
0.5
> 0xa.b
10.6875
> 10.6875 * 4
42.75
>
> 0xa.bp2 == 0xa.bP2
true
使用带有 %a
选项的 string.format
函数,Lua 便可写出这种格式的数字:
> string.format("%a", 419)
0x1.a3p+8
> 0x1.a3
1.63671875
> 419 / 0x1.a3
256.0
> 0x1p+8
256.0
> string.format("%a", 0.1)
0x1.999999999999ap-4
虽然这种格式对人类不太友好,但他保留了浮点数值的全部精度,而且转换速度比十进制数更快。
算术运算符
Lua 提供了常用的那套算术运算符:加、减、乘、除和取反,negation(一元减号)。他还支持取整除法,floor division、求模,modulo 以及求幂,exponentiation。
Lua 5.3 中引入整数的主要指导原则之一,便是 “程序员可能会选择忽略整数和浮点数之间的差异,或者完全控制每个数字的表示”注 3。因此,任何的算术运算符,都应该在处理整数和实数时,给出同样的结果。
两个整数相加,总是一个整数。减法、乘法和求反运算也是如此。对于这些运算,操作数是整数,还是带有整数的浮点数,并不重要(溢出的情况除外,我们将在 表示法的极限 小节中讨论);两种情况下的结果是一样的:
> 13 + 15
28
> 13.0 + 15.0
28.0
如果两个操作数都是整数,那么操作结果为整数;否则,操作结果就会是浮点数。如果是混合操作数,Lua 会在操作前,将整数操作数,转换为浮点数操作数:
> 13.0 + 25
38.0
> -(3 * 6.0)
-18.0
除法则并不遵循这一规则,因为两个整数的除数,不一定是整数。(用数学术语来讲,我们说整数,在除法中是不封闭的)。为了避免整数除法和浮点数除法,产生不同结果,除法总是对浮点数进行操作,并给出浮点数结果:
> 3.0 / 2.0
1.5
> 3 / 2
1.5
对于整数除法,Lua 5.3 引入了一个新的运算符,称为 下限除法,floor division,用 //
表示。顾名思义,下限除法总是会将商,向负无穷舍入,确保所有操作数都是整数。有了这样的定义,该操作便可以遵循其他算术运算符的相同规则:如果两个操作数都是整数,则结果为整数;否则,结果为浮点数(带有整数值):
> 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
下面的等式,定义了模运算符:
a % b == a - ((a // b) * b)
整数操作数,确保了结果为整数,因此该运算符,也遵循其他算术运算符的规则:如果两个操作数都是整数,则结果为整数;否则,结果为浮点数。
对于整数操作数,求模具有通常的含义,其结果总是与第二个参数的符号相同。特别是,对于任何给定正的常数 K
,表达式 x % K
的结果,总是在 [0,K-1]
的范围内,即使 x
是负数。例如,对于任何整数 i
,i % 2
的结果,总会是 0
或 1
。
对于实数的操作数,求模则有一些意想不到的用途。例如,x - x % 0.01
表示了小数点后两位数的 x
,而 x - x % 0.001
,则表示小数点后三位数的 x
:
> x = math.pi
> x
3.1415926535898
> x - x%0.01
3.14
> x - x%0.001
3.141
作为使用模运算符的另一示例,假设我们要检查一辆车,在转过一个给定角度后,是否会开始倒车。如果角度单位是度,咱们就可以使用下面的公式:
local tolerance = 10
function isturnback (angle)
angle = angle % 360
return (math.abs(angle - 180) < tolerance)
end
这一定义,甚至适用于负的角度:
> print(isturnback(-180))
true
如果我们打算使用弧度,而不是度数,则只需改变函数中的常数即可:
local tolerance = 0.17
function isturnback (angle)
angle = angle % (2*math.pi)
return (math.abs(angle - math.pi) < tolerance)
end
要将任何角度,标准化为区间 [0, 2π)
中的值,咱们只需 angle % (2 * math.pi)
这个运算。
注意:原文这里是
[[0, 2#)
,这里直接写作了[[0, 2π)
。
Lua 还提供一个幂运算符,用插入符号 (^
) 表示。与除法一样,他总是对浮点数进行运算。(整数在幂运算中不封闭,例如 2^-2
不是整数)。我们可以写下 x^0.5
,来计算 x
的平方根;写下 x^(1/3)
,来计算其立方根。
关系运算符
Lua 提供了下面这些关系运算符:
< > <= >= == ~=
全部的这些运算符,总是产生布尔值。
==
运算符,测试相等;~=
运算符,是相等的否定。我们可以对任意两个值,应用这些操作符。如果两个值类型不同,Lua 会认为,他们不相等。否则,Lua 将根据他们的类型,对他们进行比较。
数的比较,总是不会考虑其子类型;数字表示为整数,还是浮点数没有区别。重要的是其数学值。(尽管如此,比较有着相同子类型的数,效率会更高)。
数学库
The Mathematical Library
Lua 提供了一个标准的 math
库,其中包含一系列数学函数,包括了三角函数(sin
、cos
、tan
、asin
等)、对数,logarithms、四舍五入函数,rounding functions、max
和 min
、用于生成伪随机数的函数(random
),以及常数 pi
和 huge
(最大可表示数,即大多数平台上的特殊值 inf
)。
> math.sin(math.pi / 2)
1.0
> math.max(10.4, 7, -3, 20)
20
> math.huge
inf
> math.pi
3.1415926535898
全部三角函数,都以弧度为单位。我们可以使用函数 deg
和 rad
,在度和弧度之间进行转换。
> math.rad(90)
1.5707963267949
> math.deg(1.57)
89.954373835539
随机数生成器
math.random
函数,会生成伪随机数。我们可以通过三种方式调用他。当不带参数调用他时,他会返回一个在区间 [0,1)
内,均匀分布的伪随机实数。当只使用一个参数(整数 n
)调用他时,他会返回一个在区间 [1,n]
内的伪随机整数。例如,我们可以调用 math.random(6)
来模拟掷骰子的结果。最后,我们可以使用两个整数参数(l
和 u
),调用 random
,会得到一个区间为 [l,u]
的伪随机整数。
> math.random()
0.9272271007317
> math.random(100)
88
> random(100)
stdin:1: attempt to call a nil value (global 'random')
stack traceback:
stdin:1: in main chunk
[C]: in ?
> math.random(1, 100)
6
我们可以使用函数 randomseed
,为这个伪随机数生成器设置种子;该函数的唯一数字参数,就是种子。程序启动时,系统会使用固定的种子 1
初始化生成器。如果没有其他种子,程序的每次运行,都会生成相同的伪随机数序列。对于调试来说,这是一个很好的特性;但在某个游戏中,我们会一次又一次地,遇到相同的情况。解决这一问题的常用技巧,是使用当前时间作为种子,调用 math.randomseed(os.time())
。(我们将在 “函数 os.time
” 小节中,看到 os.time
)。
取整(四舍五入)函数
数学库提供了三个四舍五入函数:floor
、ceil
和 modf
。floor
向负无限舍入,ceil
向正无限舍入,modf
向零舍入。在结果适合整数时,他们会返回整数;否则,他们会返回浮点数(当然是一个整数值)。函数 modf
除了舍入值外,还会返回数字的小数部分,作为第二个结果。注 4
> 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
的下限。然而,当参数是个较大的整数值时,这种简单的加法,可能会带来错误。例如,请看接下来的代码片段:
> x = 2^52 + 1
> x
4.5035996273705e+15
> print(string.format("%d\t%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
上面这个函数,总是将半整数向上舍入(例如,2.5
将舍入为 3
)。如果我们想要无偏舍入,unbiased rounding(将半整数,舍入到最接近的偶整数),那么当 x + 0.5
是奇整数时,我们的公式就会失败:
> round(3.5)
4 --> (ok)
> round(2.5)
3 --> (wrong)
浮点数的求模运算符,再次显示了他的用处:当 x + 0.5
是奇数整数时,测试 (x % 2.0 == 0.5)
恰好为真,也就是说,当我们的公式会给到错误结果时,测试 (x % 2.0 == 0.5)
恰好为真。基于这一事实,我们就很容易定义出无偏四舍五入的函数:
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
> round(2.5)
2
> round(3.5)
4
> round(-2.5)
-2
> round(-3.5)
-4
> round(-1.5)
-2
表示法的限制
Representation Limits
大多数编程语言,都用固定的二进制位数,表示数字。因此,这些表示法,在范围和精度上,都有限制。
标准 Lua 使用了 64 位的整数。64 位整数最多可表示 2^63 - 1
的值,大约为 10^19
。(小型 Lua 则使用 32 位的整数,最多可表示到约 20 亿)。数学库为整数定义了最大值(math.maxinteger
)和最小值(math.mininteger
)两个常量。
64 位整数的最大值,是一个很大的数字:他是以美分计算地球总财富的数千倍,是世界人口的十亿倍。尽管这个数值如此之大,溢出还是时有发生。当我们进行整数运算时,如果运算结果小于 mininteger
或大于 maxinteger
,结果就会 回绕,wraps around。
用数学术语来说,“回绕” 意味着,计算结果是最小整数和最大整数之间,唯一等于模数 2^64
数学结果的数字。在计算方面,这意味着我们会丢弃最后一位进位。(最后一位进位原本将递增假设的第 65 位,即 2^64
。因此,忽略这一位并不会改变该值的模数 2^64
)。这种行为在 Lua 的所有整数算术运算中,都是一致和可预测的:
> math.maxinteger + 1 == math.mininteger
true
> math.mininteger - 1 == math.maxinteger
true
> -math.mininteger == math.maxinteger
false
> -math.mininteger == math.mininteger
true
> math.mininteger // -1 == math.mininteger
true
最大可表示整数,为 0x7ff...ffff
,也就是说,除了最高位,即信号位,the signal bit(为零 0
表示非负数),其他位都置 1
。当我们在这个数字上加 1
时,他就变成了 0x800...000
,这就是最小可表示整数。如下所示,最小整数的值,比最大整数的值大 1
:
> print(string.format("0x%x\t%d", math.maxinteger, math.maxinteger))
0x7fffffffffffffff 9223372036854775807
> print(string.format("0x%x\t%d", math.mininteger, math.mininteger))
0x8000000000000000 -9223372036854775808
对于浮点数,标准 Lua 使用双精度。他用 64 个二进制位,来表示每个数字,其中 11
位用于指数。双精度浮点数,可以表示大约 16 个有效十进制位,范围从 -10^308
到 10^308
。(小型 Lua 则使用 32 位的单精度浮点数。在这种情况下,范围是从 -10^38
到 10^38
,大约有 7 位的效十进制位)。
对于大多数具体应用来说,双精度浮点数的范围是足够大的,但我们必须始终了解其精度有限。这里的情况与使用纸笔时并无不同。如果我们用十位数表示一个数字,1/7
就会四舍五入为 0.142857142
。如果我们用十位数计算 1/7 * 7
,结果将是 0.999999994
,这与 1
不同。此外,在十进制中,有着有限表示的数,在二进制中则可以表示为无限。例如,12.7 - 20 + 7.3
这个算式,即使用双精度计算,也不会是准确的零,因为 12.7
和 7.3
在二进制中,都没有精确的有限表示(见练习 3.5)。
由于整数和浮点数,均有不同的限制,我们可以预料,当结果达到这些限制时,整数和浮点数的算术运算结果将不同:
> math.maxinteger + 2
-9223372036854775807
> math.maxinteger + 2.0
9.2233720368548e+18
在这个示例中,两个结果在数学上都是不正确的,但方式却截然不同。第一行是整数加法,所以结果回绕了。第二行是浮点加法,因此结果被四舍五入为近似值,is rounded to an approximate,如同咱们可以从下面的等式中所看到的:
> math.maxinteger + 2.0 == math.maxinteger + 1.0
true
每种表示法都有自己的长处。当然,只有浮点数可以表示小数。浮点数的范围要大得多,但他能精确表示整数的范围仅限于 [-2^53,2^53]
(尽管如此,这些都是相当大的数)。在这些范围内,我们基本上可以忽略整数和浮点数之间的差异。如果超出这些范围,我们就应该更仔细地考虑,我们所使用的表示法了。
转换
要强制将某个数转换为浮点数,我们只需将其加上 0.0
即可。整数总是可以转换成浮点数:
> -3 + 0.0
-3.0
> 0x7fffffffffffffff + 0.0
9.2233720368548e+18
任何不超过 2^53
(即 9007199254740992
)的整数,都可以精确地表示为双精度浮点数。而那些绝对值更大的整数,则会在转换为浮点数时,可能会丢失精度:
> 9007199254740991 + 0.0 == 9007199254740991
true
> 9007199254740992 + 0.0 == 9007199254740992
true
> 9007199254740993 + 0.0 == 9007199254740993
false
> 9007199254740993 + 0.0 == 9007199254740992
true
在最后一行中,转换会将整数 2^53+1
,舍入为浮点数 2^53
,从而打破了相等关系。
要强制莫个数为整数,我们可以将其与 0
,进行 OR 运算:注 5
> 2^53
9.007199254741e+15 --> (float)
> 2^53 | 0
9007199254740992 --> (integer)
只有当数可以精确地表示为整数时,Lua 才会进行这种转换,也就是说,其没有小数部分,并且在整数范围内。否则,Lua 会抛出错误:
> 3.2 | 0
stdin:1: number has no integer representation
stack traceback:
stdin:1: in main chunk
[C]: in ?
> 2^64 | 0
stdin:1: number has no integer representation
stack traceback:
stdin:1: in main chunk
[C]: in ?
> math.random(1, 3.5)
stdin:1: bad argument #2 to 'random' (number has no integer representation)
stack traceback:
[C]: in function 'math.random'
stdin:1: in main chunk
[C]: in ?
要对小数进行四舍五入,我们必须明确调用某个四舍五入函数。
另一种将数字强制转换为整数的方法,是使用 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
> cond2int(3.05)
3.05
> cond2int(-2.0)
-2
> cond2int(-2.5)
-2.5
> cond2int(2^64)
1.844674407371e+19
优先级
Precedence
Lua 中运算符优先级,如下表所示,从高到低依次排列:
^
- # ~ not --(一元运算符,unary operators)
* / // %
+ -
.. --(连接运算符,concatentation)
<< >> --(二进制移位,bitwise shifts)
& --(二进制与,bitwise AND)
~ --(二进制异或,bitwise exclusive OR)
| --(二进制或,bitwise OR)
< > <= >= ~= ==
and
or
除了指数(幂)运算,和连接运算是右结合运算外,所有二元运算符都是左结合运算。因此,下面左边的表达式,等价于右边的表达式:
a+i < b/2+1 <--> (a+i) < ((b/2)+1)
5+x^2*8 <--> 5+((x^2)*8)
a < y and y <= 2 <--> (a < y) and (y <= z)
-x^2 <--> -(x^2)
x^y^z <--> x^(y^z)
在有疑问时,请务必使用明确的括号。这比在手册中查找,要容易得多,而且别人在阅读你的代码时,也可能会有同样的疑问。
引入整数之前的 Lua
在 Lua 5.3 中引入整数,并不是偶然的,其与以前的 Lua 版本,几乎没有不兼容的地方。正如我(作者)所说的,程序员大多可以忽略整数和浮点数之间的差异。当我们忽略这些差异时,我们也可以忽略 Lua 5.3 与 Lua 5.2 之间的差异,在 Lua 5.2 中,所有数字都是浮点数。(关于数字,Lua 5.0 和 Lua 5.1 与 Lua 5.2 完全一样)。
当然,Lua 5.3 和 Lua 5.2 之间的主要不兼容性,在于整数的表示限制。Lua 5.2 最多只能表示到 2^53
精确整数,而 Lua 5.3 的限制为 2^63
。在计数时,这种差异很少成为问题。但是,当数字要表示某种通用位模式,generic bit pattern(例如,三个 20 位的整数,打包在一起)时,这种差异就会变得至关重要。
尽管 Lua 5.2 不支持整数,但整数还是以多种方式,潜入了语言。例如,用 C 语言实现的库函数,经常会得到整数参数。在这些地方,Lua 5.2 并未说明如何将浮点数,转换为整数:手册中只说了 “[该数] 会以某种非特定方式截断”。这并不是一个假设的问题;Lua 5.2 确实可以将 -3.2
转换为 -3
或 -4
,具体取决于平台。而 Lua 5.3 则精确定义了这些转换,只有当数字具有精确整数表示时,才会进行转换。
Lua 5.2 未提供函数 math.type
,是因为所有数字都有相同的子类型。Lua 5.2 没有提供常量 math.maxinteger
和 math.mininteger
,因为他没有整数。Lua 5.2 也不提供下限除法(底除),floor division,尽管他是可以。(毕竟,他的求模 modulo
运算符,已经用下限除法(底除)定义了)。
令人惊讶的是,与引入整数有关的主要问题,是 Lua 如何将数字转换为字符串。Lua 5.2 会将任何整数值,格式化为整数,不带小数点。Lua 5.3 将所有浮点数,格式化为浮点数,要么带小数点,要么带指数。因此,Lua 5.2 会将 3.0
,格式化为 "3"
,而 Lua 5.3 则会将其格式化为 "3.0"
。虽然 Lua 从未规定如何在转换中格式化数字,但许多程序都依赖于以前的行为。我们可以通过在将数字转换为字符串时,使用显式格式来解决此类问题。不过,更常见的情况是,这个问题表明,在其他地方存在更深层次的缺陷,即整数在没有充分理由的情况下,变成了浮点数。(事实上,这正是 5.3 版新格式规则的主要动机。在程序中,整数值被表示为浮点数,通常是一种不好的味道。新格式规则暴露了这些问题)。
尾注
-
我们使用与标准 Lua 相同的源文件,创建 Small Lua,只是编译时,定义了宏
LUA_32BITS
。除了数字表示的大小外,Small Lua 与标准 Lua 完全相同。 -
此特性是 Lua 5.2 中引入的。
-
摘自 Lua 5.3 参考手册。
-
正如我们将在 “多重结果” 一节中讨论的,Lua 中的一个函数可以返回多个值。
-
位运算是 Lua 5.3 中的新特性。我们将在 “位运算符” 一节中讨论它们。
练习
练习 3.1: 下面哪些是有效数字?他们的值是多少?
.0e12 .e12 0x12 0xABFG 0xA FFFF 0xFFFFFFFF
0x 0x1P10 0.1e1 0x0.1p2
练习 3.2:解释下列结果:
> math.maxinteger * 2
-2
> math.mininteger * 2
0
> math.maxinteger * math.maxinteger
1
> math.mininteger * math.mininteger
0
(请记住,整数的算术运算总是会回绕。)
练习 3.3:下面的程序将打印什么?
for i = -10, 10 do
print(i, i % 3)
end
练习 3.4: 表达式 2^3^4
的结果是什么?2^-3^4
又如何?
练习 3.5:数 12.7
等于分数 127/10
。你能把它表示成分母是 2 的幂的普通分数吗?数字 5.5 呢?
练习 3.6:给定直角圆锥的高,以及母线与轴线的夹角,请编写一个函数,来计算直角圆锥的体积。
练习 3.7:使用 math.random
,编写一个函数,生成具有标准正态分布(高斯分布)的伪随机数。