数字

到 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 还支持浮点的十六进制常量,这种常量可以有小数部分,以及二进制的指数,前缀为 pP注 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 是负数。例如,对于任何整数 ii % 2 的结果,总会是 01

对于实数的操作数,求模则有一些意想不到的用途。例如,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 库,其中包含一系列数学函数,包括了三角函数(sincostanasin 等)、对数,logarithms、四舍五入函数,rounding functions、maxmin、用于生成伪随机数的函数(random),以及常数 pihuge(最大可表示数,即大多数平台上的特殊值 inf)。

> math.sin(math.pi / 2)
1.0
> math.max(10.4, 7, -3, 20)
20
> math.huge
inf
> math.pi
3.1415926535898

全部三角函数,都以弧度为单位。我们可以使用函数 degrad,在度和弧度之间进行转换。

> math.rad(90)
1.5707963267949
> math.deg(1.57)
89.954373835539

随机数生成器

math.random 函数,会生成伪随机数。我们可以通过三种方式调用他。当不带参数调用他时,他会返回一个在区间 [0,1) 内,均匀分布的伪随机实数。当只使用一个参数(整数 n)调用他时,他会返回一个在区间 [1,n] 内的伪随机整数。例如,我们可以调用 math.random(6) 来模拟掷骰子的结果。最后,我们可以使用两个整数参数(lu),调用 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)。

取整(四舍五入)函数

数学库提供了三个四舍五入函数:floorceilmodffloor 向负无限舍入,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^30810^308。(小型 Lua 则使用 32 位的单精度浮点数。在这种情况下,范围是从 -10^3810^38,大约有 7 位的效十进制位)。

对于大多数具体应用来说,双精度浮点数的范围是足够大的,但我们必须始终了解其精度有限。这里的情况与使用纸笔时并无不同。如果我们用十位数表示一个数字,1/7 就会四舍五入为 0.142857142。如果我们用十位数计算 1/7 * 7,结果将是 0.999999994,这与 1 不同。此外,在十进制中,有着有限表示的数,在二进制中则可以表示为无限。例如,12.7 - 20 + 7.3 这个算式,即使用双精度计算,也不会是准确的零,因为 12.77.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.maxintegermath.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 版新格式规则的主要动机。在程序中,整数值被表示为浮点数,通常是一种不好的味道。新格式规则暴露了这些问题)。

尾注

  1. 我们使用与标准 Lua 相同的源文件,创建 Small Lua,只是编译时,定义了宏 LUA_32BITS。除了数字表示的大小外,Small Lua 与标准 Lua 完全相同。

  2. 此特性是 Lua 5.2 中引入的。

  3. 摘自 Lua 5.3 参考手册。

  4. 正如我们将在 “多重结果” 一节中讨论的,Lua 中的一个函数可以返回多个值。

  5. 位运算是 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,编写一个函数,生成具有标准正态分布(高斯分布)的伪随机数。

Last change: 2024-07-23, commit: 1e691f0