5.2 整数

我们在前面说过,整数类型又被分为有符号类型和无符号类型。后两者分别包含了 6 种和 5 种具体类型。我们为了表示这些类型的值而输入的内容又被称为整数字面量。比如,上一章示例中的1242020都是整数字面量。

5.2.1 类型与取值

整数类型的名称大都很直观,并且它们的宽度也都已经体现在名称中了。详见下表。

表 5-1 整数类型及其取值

类型名 是否有符号? 其值占用的比特数 最小值 最大值
Int8 8 -2^7 2^7 - 1
UInt8 8 0 2^8 - 1
Int16 16 -2^15 2^15 - 1
UInt16 16 0 2^16 - 1
Int32 32 -2^31 2^31 - 1
UInt32 32 0 2^32 - 1
Int64 64 -2^63 2^63 - 1
UInt64 64 0 2^64 - 1
Int128 128 -2^127 2^127 - 1
UInt128 128 0 2^128 - 1

我们最好记住该表中各个类型的最小值和最大值。这并不困难,因为它们是有规律可循的。不过,实在记不住也没有关系。通过调用typemin函数和typemax函数,我们可以分别获得某一个整数类型能够表示的最小值和最大值,例如:

  1. julia> typemin(Int8), typemax(Int8)
  2. (-128, 127)
  3. julia> typemin(UInt8), typemax(UInt8)
  4. (0x00, 0xff)
  5. julia>

严格来说,Bool类型也属于整数类型。因为它与SignedUnsigned一样,也是Integer类型的直接子类型。Bool类型的宽度(也就是其值占用的比特数)是8,最小值是0(即false),最大值是1(即true)。

此外,Julia 还定义了IntUIntInt代表了有符号整数的默认类型。在 32 位的计算机系统中,它们分别是Int32UInt32的别名。而在 64 位的计算机系统中,它们分别是Int64UInt64的别名。如此一来,我们在 REPL 环境中随便输入一个整数字面量,就能猜出它的类型:

  1. julia> typeof(2020) # 在 32 位的计算机系统中
  2. Int32
  3. julia>
  1. julia> typeof(2020) # 在 64 位的计算机系统中
  2. Int64
  3. julia>

这完全取决于我们的计算机系统的位数(或者说字宽)。顺便说一句,我们可以通过访问常量Sys.WORD_SIZE来获知自己的计算机系统的字宽。

不过,对于较大的整数,Julia 会自动使用宽度更大的整数类型,例如:

  1. julia> typeof(1234567890123456789)
  2. Int64
  3. julia> typeof(12345678901234567890)
  4. Int128
  5. julia>

注意,在这个例子中,整数字面量的类型是否为Int128,依据的不是字面量的长度,而是字面量表示的整数是否大于Int64类型的最大值。

5.2.2 表示方法

与前面的有符号整数不同,无符号整数会使用以0x为前缀的十六进制形式来表示。比如:

  1. julia> UInt32(2020)
  2. 0x000007e4
  3. julia> UInt64(2020)
  4. 0x00000000000007e4
  5. julia>

我们都知道,在这些十六进制整数中,字母af分别代表了十进制整数1015,并且大写这些字母也是可以的。注意,无符号整数值的类型会由字面量本身决定:

  1. julia> typeof(0x01)
  2. UInt8
  3. julia> typeof(0x001)
  4. UInt16
  5. julia> typeof(0x00001)
  6. UInt32
  7. julia> typeof(0x000000001)
  8. UInt64
  9. julia> typeof(0x00000000000000001)
  10. UInt128
  11. julia>

无符号整数值0x01只需占用 8 个比特(1 位的十六进制数相当于 4 位的二进制数),因此使用UInt8类型就足够了。无符号整数值0x001占用的比特是 12 个,超出了UInt8类型的位数,所以就需要使用UInt16类型。而0x00001的占位是 20 个,所以需要使用UInt32类型。以此类推。总之,一个无符号整数值的默认类型将会是能够容纳它的那个宽度最小的类型。

除了十六进制之外,我们还可以使用二进制或八进制的形式来表示无符号整数值。比如:

  1. julia> 0b00000001
  2. 0x01
  3. julia> 0o001
  4. 0x01
  5. julia>

0b为前缀的整数字面量就是以二进制形式表示的整数,而以0o为前缀的整数字面量则是以八进制形式表示的整数。在这里,数字1至少需要 8 位的二进制数或 3 位的八进制数或 2 位的十六进制数来表示。即使我们输入的位数不够也没有关系,Julia 会自动帮我们在高位补0以填满至相应类型的位数(这里是 8 个比特):

  1. julia> 0b001
  2. 0x01
  3. julia> 0o01
  4. 0x01
  5. julia> 0x1
  6. 0x01
  7. julia>

对于更大的无符号整数值的字面量来说也是类似的。

注意,二进制、八进制和十六进制的字面量可以表示无符号的整数值,但不能表示有符号的整数值。虽然我们可以在这些字面量的前面添加负号-,但是它们表示的依然是无符号的整数值。例如:

  1. julia> -0x01, typeof(-0x01), Int16(-0x01)
  2. (0xff, UInt8, 255)
  3. julia>

不要被字面量-0x01中的负号迷惑,它表示的值的类型仍然是UInt80xff实际上是负的0x01(也就是-1)的补码。但由于十六进制字面量表示的整数值只能是无符号的,所以 Julia 会把它视为一个无符号整数值的原码。如此一来,字面量-0x01代表的整数值就是255

顺便说一下,我们可以使用下划线_作为数值字面量中的数字分隔符。至于划分的具体间隔,Julia 并没有做硬性的规定。例如:

  1. julia> 2_020, 0x000_01, 0b000_000_01, -0x0_1
  2. (2020, 0x00000001, 0x01, 0xff)
  3. julia>

5.2.3 关于溢出

我们已知每个整数类型的最小值和最大值。当一个整数值超出了其类型的取值范围时,我们就说这个值溢出(overflow)了。

以 64 位的计算机系统为例,Julia 对整数值溢出有两种处理措施,具体如下:

  • 对于其类型的宽度小于64的整数值,值不变,其类型会被提升到Int64
  • 对于其类型的宽度等于或大于64的整数值,其类型不变,对值采取环绕式(wraparound)处理。

也就是说,对于Int8Int16Int32‌、UInt8UInt16UInt32这 6 个类型,Julia 会把溢出值的类型自动地转换为Int64。这样的话,这些值就不再是溢出的了。

对于宽度更大的整数类型,Julia 会采取不同的应对措施——环绕式(wraparound)处理。这是什么意思呢?比如,当一个Int64类型的整数值比这个类型的最大值还要大1的时候,该值就会变成这个类型的最小值。相对应的,当这个类型的整数值比其最小值还要小1的时候,该值就会变成这个类型的最大值。示例如下:

  1. julia> int1 = typemax(Int64)
  2. 9223372036854775807
  3. julia> int2 = int1 + 1
  4. -9223372036854775808
  5. julia> int2 == typemin(Int64)
  6. true
  7. julia> int3 = int2 - 1
  8. 9223372036854775807
  9. julia> int3 == typemax(Int64)
  10. true
  11. julia>

可以想象一下,对于一个宽度小于64的整数类型,它的所有可取值共同形成了一根又长又直的棍子。棍子上的值以由小到大的顺序从左到右排列。棍子的最左端是它的最小值,而最右端是它的最大值。

但对于像Int64这样的整数类型来说,其所有可取值共同形成的就不再是一根棍子了,而是一个圆环。这就好像把原来的棍子掰弯并首尾相接了一样。当该类型的值从它的最大值变更为最大值再加1时,就好似从圆环接缝的右侧移动一格,到了接缝左侧。相对应的,当该类型的值从它的最小值变更为最小值再减1时,就好像从圆环接缝的左侧移动一格,到了接缝右侧。这样的处理方式就叫做对整数溢出的环绕式处理。

图 5-1 对整数溢出的环绕式处理 图 5-1 对整数溢出的环绕式处理

总之,对于Int64Int128UInt64UInt128这 4 个类型,Julia 会对溢出值做环绕式处理。

如果你需要的是不会溢出的整数类型,那么可以使用BigInt。它的值的大小只受限于当前计算机的内存空间。

5.2.4 BigInt

BigInt类型属于有符号的整数类型。它表示的数值可以是非常大的正整数,也可以是非常小的负整数。由此,我们可以说,它的值可以是任意精度的。

与其他的整数类型一样,其实例的构造函数与类型拥有相同的名称。并且,我们还可以使用一种非常规的字符串来构造它的值。例如:

  1. julia> BigInt(1234567890123456789012345678901234567890)
  2. 1234567890123456789012345678901234567890
  3. julia> typeof(ans)
  4. BigInt
  5. julia> big"1234567890123456789012345678901234567890"
  6. 1234567890123456789012345678901234567890
  7. julia> typeof(ans)
  8. BigInt
  9. julia>

可以看到,我们把一串很长的数字传给了BigInt函数,并由此构造了一个BigInt类型的值。实际上,BigInt函数接受的唯一参数可以是任意长度的整数字面量,也可以是任何其他整数类型的值。

甚至,这个构造函数的参数值还可以是像big"1234"这样的非常规字符串。不过,我们没有必要这么做。因为big"1234"本身就能够表示一个BigInt类型的实例。更宽泛地讲,在一个内容形似整数的字符串前添加big这 3 个字母就可以把它变成一个BigInt类型的值。

另外,任何溢出的整数值的类型都不会被自动地转换成BigInt。如有需要,我们只能手动地进行类型转换。

最后,你需要了解的是,虽然BigInt直接继承自Signed类型,但它是一个比较特殊的整数类型。它被定义在了Base.GMP包中,而其他的整数类型的定义都在Core包中。GMP 指的是 GNU Multiple Precision Arithmetic Library,可以翻译为多精度算术库。Julia 中的Base.GMP包实际上只是对这个库的再次封装而已。虽然如此,这样一个类型的值却可以直接与其他类型的数值一起做数学运算。这主要得益于 Julia 中数值类型的层次结构,以及它的类型提升和转换机制。