Questing for Swift Source Code - 整数类型

144
作者 星夜暮晨
2016.01.03 20:36 字数 8410

"Questing for Swift Source Code" 系列是我学习 Swift 源码的心得和记录,内容主要是 Swift 源代码的相关分析和探究,如果您对 Swift 源代码也很感兴趣的话,欢迎阅读这个系列的文章。

Swift 源码极其庞大,里面所使用的语言囊括了 Swift、 Python、C++等,因此我觉得这是一个巨大的坑,我不知道能不能把它填完,不过我会尽力而为的~^_^

我们将一起通过分析和学习 Swift 源码,来体会 Swift 这个最前沿编程语言的设计思想,并且从中找出一些鲜为人知的用例!

本文是该系列的第二篇文章,将介绍关于 Swift 中整数类型的定义和实现。


上一节中,我们讲述了 Swift 基本类型之一的 Bool 类型的实现,但是,您很可能已经发现了,在 core 文件夹下,并没有 IntDouble之类的文件存在。那么这些类型去哪里了呢?

在 core 文件夹下,我们发现了名为 FixedPoint.swift.gyb 的文件,在其中我们找到了有关于 Int 的定义,原来它藏在这个地方。

什么是 gyb 文件?

Gyb 的全称是 Generate Your Boilerplate (生成你的样板),这是 Swift 团队用以预处理的文件,基本语法为 Python 构成,用以生成相应的 Swift 代码并编译。

那么为什么要这么做呢?举个例子,Int 有许许多多的变种,比如说 Int8Int16Int32等等。这些变种除了某些属性或者某些特征不同之外,基本上都有着相同的行为和代码。对于 Swift 团队来说,直接复制代码显然是不合理的需求,不说工作量的大小,万一要修改某一部分的核心需求的话,那么就不得不修改所有的变种,这显然是一件费时费力的事情。

因此,gyb 文件就很好地解决了这个问题。通过 Python 和苹果所编写的编译器,就相当于一个“自动化工厂”一样,可以将内嵌的 Swift 代码导出。

单行 Python 代码将以 % 符号开头,一组 Python 代码也可能会使用 %{...}% 包含。编译器读取到这个符号之后,就会进行识别,从而执行对应的 Python 代码。而在要生成的 Swift 代码中,${...} 就类似于 \(...) 字符串插值功能,将对应的 Python 变量插入到该位置处。简而言之,你可以将 % 起头的代码视为真实运行的代码部分,而将其他代码视为“字符串”,这样就更好理解了。

gyb文件将由位于 utils 文件夹下的 gyb 文件负责编译和处理。

循环体

因此,在这个文件中,Swift 团队采取了循环的形式,像构造字符串那样简单地构造 Int8、Int16` 之类的整数类型。遍历的部分代码如下:

% for self_ty in all_integer_types(word_bits):
%   bits = self_ty.bits
%   signed = self_ty.is_signed
%   (sign, ext) = ('s', 'sext') if signed else ('u', 'zext')
%   Self = self_ty.stdlib_name
%   BuiltinName = self_ty.builtin_name
%   OtherSelf = self_ty.get_opposite_signedness().stdlib_name
...

让我们逐行来进行分析:

all_interger_types 是定义在 SwiftIntTypes.py (这个文件位于 utils 文件夹)中的命令,定义如下:

def all_integer_types(word_bits):
    for bitwidth in _all_integer_type_bitwidths:
        for is_signed in [ False, True ]:
            yield SwiftIntegerType(is_word=False, bits=bitwidth,
                is_signed=is_signed)

    for is_signed in [ False, True ]:
        yield SwiftIntegerType(is_word=True, bits=word_bits,
            is_signed=is_signed)

这里用 Python 定义 (def) 了一个函数,这个函数使用 for in_all_integer_type_bitwidths(也是预定义在此文件中的,其值为 [8, 16, 32, 64])进行遍历,并且再次对 [False, True] 数组进行遍历,然后依次生成所需要返回的值 SwiftIntegerType类,最后用 yield 将值归纳、集合并返回。

简而言之,这个操作能够产生所有需要添加的 Int 类型,all_interger_types 的最终结果将是一个类型为 SwiftIntegerType 类的数组。

word_bits 也是预定义好的,它的声明为:

word_bits = int(CMAKE_SIZEOF_VOID_P) * 8

这个是内置的双字 (DWORD) 类型的位数,不同架构的机器该位数不同,在 32 位机器上字长为 32 位,而在 64 位机器上字长变成了 64 位。

让我们回到最初的循环体来。遍历过程中,我们将依次读取要生成 Int 类型的位数 (bits)、判断其是有符号数还是无符号数 (signed)、读取其名字 (Self)、内置名称 (BuiltinName)、标记 (sign) 及其对应的有(无)符号数名字 (OtherSelf)。

名字的构造规则为:

如果是无符号数,那么 "Int" 前面加上 "U",如果不是字类型,那么 "Int" 后面加上对应的位数。因此,这里我们终将构造 Int8Int16Int32Int64UInt8UInt16UInt32UInt64UIntInt类型。

定义

首先我们可以看到 Swift 中对整数类型的定义:

public struct ${Self}: ${'SignedIntegerType' if sign == 's' else 'UnsignedIntegerType'}, Comparable, Equatable {

我们前面已经介绍过,${Value}可以将 python 中定义的变量值 Value 直接替换到这个标识符的所在位置。

我们看到,和 Bool 类似,整数类型也是使用结构体进行定义的。除此之外,它还实现了 SignedIntegerType( 无符号整数是 UnsignedIntegerType )、Comparable 以及 Equatable 协议。

随后,就是整数类型中的实际值了:

public var _value: Builtin.${BuiltinName}

我们在上一节中已经介绍过,Builtin 是 Swift 内置库之一,虽然 BuiltinName 不同,但它们所最终生成的整数类型仍然还是相同的,它们都是 BuiltinIntegerType 类型。

初始化

我们下面来查看定义的整数类型初始化方法。

Swift 内部定义了几种初始化方法,让我们分别来看一下它们的定义:

init()

@_transparent public 
init() {
  let maxWidthZero: IntMax = 0
  self._value = Builtin.truncOrBitCast_Int${int_max_bits}_${BuiltinName}(maxWidthZero._value)
}

这个方法将创建一个初始值为 0 的整数类型,IntMax是内部预定义的一个类型,由 "Int" + "int_max_bits" 构成,在这里,一般来说使用的都是 Int64

随后,将会采用我们已经介绍过的内置转换方法,将 IntMax 转换为相应类型的数值,最后进行赋值。

init(0)

@_transparent public
  init(_ value: ${Self}) { self = value }

这个方法比较直白,使用相同类型来初始化相同类型。

其他初始化方法

这几个初始化方法由于我们并不能在外面使用 (因为使用到了 Builtin),并且实现也非常简单,因此我们就只在这里列举出来,不进行介绍了。

@_transparent public
init(_ _v: Builtin.${BuiltinName})

@_transparent public
init(_bits: Builtin.${BuiltinName})

Int 独占的方法

这里通过 if self_ty.is_word 判断语句指定了几种只有 Int 类型才有的方法:

init()

@_transparent
public // @testable
init(_ v: Builtin.Word) {
% if BuiltinName == 'Int32':
  self._value = Builtin.truncOrBitCast_Word_Int32(v)
% elif BuiltinName == 'Int64':
  self._value = Builtin.zextOrBitCast_Word_Int64(v)
% end
}

这个初始化方法很简单,它可以将内置的 Builtin.Word 类型转换为相应的 Int32 或者 Int64 类型。再提及一遍,这里的 truncOrBitCast_Word_Int32zextOrBitCast_Word_Int64 都是 LLVM 内置的方法,可以对类型进行转换。

_builtinWordValue

这是一个内置的只读属性,用来将当前 Int 的 value 值转换为 Builtin.Word 类型并输出,和上面的那个初始化方法相反。

@_transparent
public var _builtinWordValue: Builtin.Word

有趣的是,虽然这个只读属性不能够被代码提示显示出来,我们仍然可以使用它,虽然无法查看它的值。

let wordValue = 1._builtinWordValue // 不透明的类型

高字节序和低字节序

在整数定义中,有一系列方法是 Int8 所没有的,那就是对 Big Endian (高字节序) 和 Little Endian (低字节序) 的处理。

什么是字节序?

在我们继续之前,先来看一下所谓字节序的涵义。

顾名思义,所谓字节序就是指字节的顺序。低字节序就是让低位字节排放在内存的低地址端,高位字节排放在内存的高地址端;高字节序就是高位字节排放在内存的低地址端,低位字节排放在内存的高地址端。

我们可以列一个清晰的图表来更好说明这个概念,假设我们要存储 0x01234567 这个数字,那么它的存储顺序就应该是这个样子的:

低字节序
低位地址 -------> 高位地址

地址 0x100 0x101 0x102 0x103
... 67 45 23 01

高字节序
低位地址 -------> 高位地址

地址 0x100 0x101 0x102 0x103
... 01 23 45 67

字节序的初始化方法

那么理解了低字节序和高字节序的概念后,后面的事情就十分简单了:

@_transparent public
init(bigEndian value: ${Self}) {
#if arch(i386) || arch(x86_64) || arch(arm) || arch(arm64)
  self = ${Self}(Builtin.int_bswap_${BuiltinName}(value._value) )
#else
  _UnsupportedArchitectureError()
#endif
}

@_transparent public
init(littleEndian value: ${Self}) {
#if arch(i386) || arch(x86_64) || arch(arm) || arch(arm64)
  self = value
#else
  _UnsupportedArchitectureError()
#endif
}

我们可以从中了解到,iOS 中(以及在 i386、x86_64、arm、arm64 架构中)中采取的是低字节序的顺序,这样做首先是因为 CPU 的架构默认的是低字节序,其次是因为在数据类型转换的时候(尤其是指针转换)不用考虑地址问题。

不管怎么说,我们可以看到,目前在 Swift 的实现中,只支持上述的几种架构模式,其余的架构就会调用 _UnsupportedArchitectureError() 这么一个全局函数,弹出错误,提示架构不支持。对于这两个初始化方法来说,如果有必要,会自行进行字节顺序的转换。

如果要进行高字节序的转换的话,那么会调用内置的 int_bswap_${BuiltinName} 方法进行转换。

获取对应字节序的整数类型

我们还可以获取某个整数类型对应字节序的类型,通过以下两个只读属性就可以获取:

public var bigEndian: ${Self}

public var littleEndian: ${Self}

这两个只读属性的操作也是与其对应的初始化方法相反,成套配对的。

获取相反顺序的字节序类型

此外,它还提供了一个只读属性,不管当前该整数的字节序是多少,都会去获取相反顺序的字节序类型。

public var byteSwapped: ${Self}

举个例子

比如说,我们可以创建一个正常的 Int 类型:

var int: Int16 = 1

我们已经知道,Swift 采取的是默认的低字节序进行存储的,因此我们使用 init(littleEndian:) 方法进行初始化是完全没有任何问题的:

let littleInt = Int16(littleEndian: int)  // 输出为 1
let littleInt2 = int.littleEndian // 类似的操作,输出为 1

但是如果使用 init(bigEndian:)方法进行初始化的话就出问题了:

let bigInt = Int16(bigEndian: int) // 输出为 256
let bigInt2 = int.bigEndian // 输出为 256

我们知道,在内存中该数字的存储从低字节到高字节应该是 "1|0" (0x0001),也就是以 8 个比特为 1 字节,那么我们将其转换为高字节序的话就会变成 "0 | 1",因此实际的输出值也就变成了0x0100,也就是 256 了。

如果我们使用 init(bigEndian:) 对转换后的高字节序进行初始化也是可以的:

let bigInt3 = Int16(bigEndian: bigInt)  // 输出为 1

此外,还可以对读取 byteSwapped 属性获取对立的字节序输出:

let swapInt = bigInt.byteSwapped  // 输出为 1

最大值和最小值

整数类型中最方便的就是获取其最大值和最小值了。

max = maskBits((bits - 1) if signed else bits)
@_transparent public
static var max: ${Self} { return ${max} }
@_transparent public
static var min: ${Self} { return ${'-%s-1' % max if signed else '0'} }

整数类型的最大值获取通过 Python 来进行,定义函数如下:

def hexify(n):
    z = '%X' % n
    l = len(z)
    r = []
    while z:
        r.insert(0, z[-4:])
        z = z[:-4]
    return '0x' + '_'.join(r)

def maskBits(n):
    return hexify((1 << n) - 1)

对于有符号数来说,传递进 maskBits 函数中的参数将是 bits - 1,而无符号数则是 bits。还记得 bits 是什么吗,是当前整数类型的位数。那么让我们来看一下核心的 hexify 函数。

  1. 将数字 n 转换为十六进制
    • '%X' 的涵义是使用十六进制格式化后面的输入值
  2. 循环执行一段操作,直到 z 变为 0 为止。
    • Python 中对于判断的定义和 C 一样,0 为 false
  3. 向 r 数组中插入经切片后的数字。
    • 所谓切片,就是那段奇怪的 [-4:] 语法。序列名后跟一个方括号,方括号中有一对可选的数字,并用冒号分割。切片操作符中的第一个数(冒号之前)表示切片开始的位置,第二个数(冒号之后)表示切片到哪里结束。如果不指定第一个数,Python就从序列首开始。如果没有指定第二个数,则Python会停止在序列尾。负数用在从序列尾开始计算的位置。
  4. 返回 0x_r 构成的最终数字。

这看起来有点难以理解,没关系,我们以一个实际例子再来说明一下。假设我们要获取 Int8 的最大值,那么我们可以得知其 bits 值为8,那么传递给 maskBits 函数的 n 值就是 7(对于无符号整数来说不用减 1),经过计算后,传递给 hexify 函数的值为 01111111。

hexify 函数中,我们能够得到对应的十六进制数字为 0x7F,然后依次执行切片操作,依次将末尾的数放到开头,然后将末尾截断,然后将 0x_7F 返回出去。这样我们也就得到了 Int8 的最大值:127。

获取最小值就更为简单了,如果是无符号整数,那么它的最小值肯定是 0,如果是有符号整数,那么给其最大值添加一个负号再减 1 即可。是不是非常作弊的做法!但是却可以省却很多计算步骤。

获取整数位数和字节数

在实现当中,Swift 还有两个方法,可以获取整数的位数 (Bits) 和字节数 (Bytes):

public static var _sizeInBits: ${Self} { return ${bits} }
public static var _sizeInBytes: ${Self} { return ${bits}/8 }

我发现一个很有意思的事情,那就是虽然在我们平时使用的 Swift 当中 (Xcode 7.2),并没有列出这两个理应当“私有” (命名前加了_) 只读属性,但是由于某种特殊的原因,我们仍然可以使用 _sizeInBits 属性获取字节,只不过你没法得到代码提示罢了:

var bits = Int._sizeInBits  // 输出为 64
var bytes = Int._sizeInBytes  // 报错,提示没有此成员

从其他整数类型构建

这一系列方法都写在了一个扩展当中,主要功能是从其他整数类型进行构建。比如说 Int 类型可以使用构造器对 Int8Int16等所有其他的整数类型完成本身的值构建。

简单初始化方法

这里的实现思路和整数类型本身十分相似,它仍然使用循环来构造这几个构造器。这部分实现代码的重点如下:

let srcNotWord = v._value
%
%   if srcBits == bits and srcSign == sign:
let dstNotWord = srcNotWord
%
%   elif srcBits == bits:
let tmp = Builtin.${srcSign}_to_${sign}_checked_conversion_Int${srcBits}(srcNotWord)
Builtin.condfail(tmp.1)
let dstNotWord = tmp.0
%
%   elif srcBits > bits:
let tmp = Builtin.${srcSign}_to_${sign}_checked_trunc_Int${srcBits}_Int${bits}(srcNotWord)
Builtin.condfail(tmp.1)
let dstNotWord = tmp.0
%
%   elif srcSigned and not signed:
let tmp = Builtin.s_to_u_checked_conversion_Int${srcBits}(srcNotWord)
Builtin.condfail(tmp.1)
let dstNotWord = Builtin.${srcExt}_Int${srcBits}_Int${bits}(tmp.0)
%
%   else:
let dstNotWord = Builtin.${srcExt}_Int${srcBits}_Int${bits}(srcNotWord)
%   end
  • 如果位数相同,并且符号相同的话,那么直接使用就好了。(比如 IntInt64 的转换)
  • 如果仅仅是位数相同,但是符号不同的话,那么就使用内置的 ${srcSign}_to_${sign}_checked_conversion_Int${srcBits} 函数执行转换 (比如 UIntInt 的转换)。这个转换会导致符号的丢失,并且会进行溢出检查,你不能将负数的 Int 转换为 UInt
  • 如果要转换的位数大于当前位数,那么使用内置的 ${srcSign}_to_${sign}_checked_trunc_Int${srcBits}_Int${bits} 函数执行转换 (比如 Int8 转换位 Int16)。
  • 如果要转换的位数小于当前位数,并且目标类型是有符号而当前是无符号的话,那么就使用 s_to_u_checked_conversion_Int${srcBits} 函数执行转换 (比如 UInt16 转换为 Int8)。同样,溢出检查仍会进行。
  • 整体的转换结果会返回两个值,一个是对应的转换之后的值,一个是是否发生溢出的标识 (Builtin.Int1 类型)。这里调用 Builtin.condfail()函数来检查是否出现溢出,如果出现溢出就会报错提示。
  • dstNotWord 保存经转换之后的值。

位模式转换

在谈论模式转换之前我们需要谈一谈所谓的 Bit Pattern (位模式) 的概念。

所谓位模式,指的是某个数值在内存里的二进制形态。比如,对于 var int = 17 这个数来说,其对应的位模式就是 "00.....0010001" 这个二进制数。

那么我们就可以看到这个用来进行位模式转换的方法了:

@_transparent
public init(truncatingBitPattern: ${Src}) {
%
  let srcNotWord = truncatingBitPattern._value
%
%     if self_ty.bits == src_ty.bits:
  let dstNotWord = srcNotWord
%     else:
  let dstNotWord = Builtin.trunc_Int${srcBits}_Int${bits}(srcNotWord)
%     end
%
  self._value = dstNotWord
}

在这个方法中,如果要转换的数和目标数类型的位数是相同的话,那么就直接赋值读取即可,而如果不同的话,则要调用内置的 trunc_Int${srcBits}_Int${bits} 函数来执行“截断功能”。

能拥有这个转换方法的类型以及能够转换的类型极其有限,有一个专门的函数 should_define_truncating_bit_pattern_init 定义了以下的几条规则:

  • 本类型不能和目标类型相同
  • 相同位长的有符号和无符号类型的转换也是禁止的
  • 本类型位长必须大于目标类型的位长

我们知道,Swift 是一门强类型语言,并且在执行数字转换的时候会进行溢出和范围的检查。因此,我们这样将 Int16 转换为 Int8 是不可能进行的,因为这发生了溢出:

let minInt16 = Int16.min
let con2Int8 = Int8(minInt16)  // 运行时错误,范围异常

但是,我们有些时候需要使用类似于“位截断”的方式来强制执行这种转换操作,不管其原值是多少,我们就只读取能在既有内存中存放并显示的值就可以了(这个操作在 C 中非常常见),那么这时候就需要使用 init(truncatingBitPattern:) 构造方法了。

这个构造方法会抹去新类型所不需要的内存部分(实际上也是调用了 C 本身内部所既有的功能),从而实现所谓的“位截断”功能。比如说,上面的操作换成这个方法后就可以进行了:

let con2Int8ByTrunc = Int8(truncatingBitPattern: minInt16) // 输出为 0

我们知道在内存中,有符号数一般都是用其开头的第一位来表示数值的正负的,"0" 为正,"1" 为负。在执行这个转换中,前面的部分就会被直接删掉。在上面的例子中,minInt16 的表示类型应该是:

| 1 0 0 0 0 0 0 0 | 0 0 0 0 0 0 0 0 |

执行转换后就变成了:

| 0 0 0 0 0 0 0 0 |

因此,我们就得到了我们的转换结果:0。

而对于相同位长,但是符号不同的转换来说,结果也是很“二进制”化的。对于 Int8UInt8 来说,相同的 1 1 1 1 1 1 1 1二进制表达方式,一个代表的是 -1,而一个则代表的是 UInt8.max

有符号数和无符号数之间的位模式转换

在上面我们提到过,init(truncatingBitPattern:) 方法中相同位长的有符号和无符号类型的转换也是禁止的,因此我们不能用这个方法在 IntUInt 之间进行转换,它根本没有提供给我们这样的方法。如果我们一定要这么做的话,我们应该怎么办呢?

答案就是采用另一个转换方法 init(bitPattern:) 了,这个方法专门为有符号数和无符号数之间进行转换。它的定义十分简单,是上面那个转换方法中实现的一部分而已。

整数类型的协议

整数类型中实现的协议非常庞大,为此我为其协议族专门画了一幅图,结果吓了一跳:


整数类型的协议

因此,我们接下来的任务就是对这些协议进行攻关,看看 Swift 是如何让整数类型实现这些协议的。

IntegerLiteralConvertible 协议

这里有两个协议,是整数类型的字面量协议扩展,实现这两个协议之后就可以使用整数字面量直接赋值了。

_BuiltinIntegerLiteralConvertible 需要实现的是 public init(_builtinIntegerLiteral value: Builtin.Int${builtinIntLiteralBits}) 方法,IntegerLiteralConvertible 需要实现的是 public init(integerLiteral value: Self.IntegerLiteralType) 方法。

而对于 _BuiltinIntegerLiteralConvertible 协议方法来说,则有点不同之处:

@_transparent public
init(_builtinIntegerLiteral value: Builtin.Int${builtinIntLiteralBits}) {
  self = ${Self}(Builtin.s_to_${sign}_checked_trunc_Int${builtinIntLiteralBits}_${BuiltinName}(value).0)
}

在这里,builtinIntLiteralBits 对应的数值是 2048,也就是说,这里要使用的参数是 Int2048,这是一个非常非常大的整数类型,很遗憾,我们在平时使用时是无法创建这个类型的。

你可以仿照上一节介绍的 BooleanLiteral 协议自己实现 IntegerLiteralConvertible 协议哦~

CustomStringConvertible 协议

这是一个 Swift 中绝大多数类型都实现的协议,实现此协议的类型都可以产生一个“输出流”,从而使得可以用 print 之类的函数打印出来。

整数实现此协议后,你就会发现它能够显示出其字面量的字符串,我们可以来查看它的定义:

extension ${Self} : CustomStringConvertible {
  public var description: String {
% if signed:
    return _int64ToString(self.toIntMax())
% else:
    return _uint64ToString(self.toUIntMax())
% end
  }
}

可以看出,在这个方法中,大致思路是将自身转换为 Int64 (或者是 UInt64) 类型,然后再转换为 String 类型。

% (U, un) = ('','') if signed else ('U','un')
@_transparent public
func to${U}IntMax() -> ${U}IntMax {
  return ${'self' if Self == U+'Int%s'%int_max_bits else U+'IntMax(self)'}
}

% if not signed:
@_transparent public
func toIntMax() -> IntMax {
  return IntMax(toUIntMax())
}

对于有符号数来说,它生成上面那个 toIntMax 函数;对于无符号数来说,两个函数它都会生成。它们都将调用 IntMax() 构造方法构造一个新的 IntMax 类型的数。

对于无符号数来说,它的 toIntMax() 函数和 toUIntMax() 函数的作用都是一致的。

如果当前调用此函数的类型已经是 IntMax 类型的话,那么就不执行操作了,直接返回本身。这样可以减少资源的耗费和占用。

至于 Int64 转换为 String 的函数,它是定义在 Runtime 里面的,我们到那时候再对其进行详细介绍,这里只要知晓它的功能就可以了。

因此,在这个方法中,我们也可以了解到一个非常好用的转换函数:toIntMax(),可以轻易地将当前的整数类型快速转换为 Int64 类型。

Hashable 协议

这个协议用来实现哈希值的实现,我们要记住这样一个定理:

当我们说某个相同类型的对象 A 和对象 B 相等时,也就是判断 A == B 时,实际上判断的都是它们的哈希值,也就是判断 A.hashValue == B.hashValue

因此,我们可以来查看一下整数类型的 Hashable 协议是如何实现的。

public var hashValue: Int {
% if bits <= word_bits and signed:
  return Int(self)
% elif bits <= word_bits and not signed:
  return Int(${OtherSelf}(bitPattern: self))
% elif bits == word_bits * 2:
  return
    Int(truncatingBitPattern: self) ^
    Int(truncatingBitPattern: self >> 32)
% else:
  _Unimplemented()
% end
}

其转换规则如下:

  • 对于小于等于当前机器位数的有符号整数,直接调用 init(_ v:) 转换方法就可以进行转换了。
  • 对于小于等于当前机器位数的无符号整数,则要调用 init(bitPattern:) 将其强制转换为对应的有符号整数。
  • 对于 32 位机器来说,如果当前整数为 64 位的话,由于其本身不支持 64 位地址,因此使用 init(truncatingBitPattern:) 将 64 位整数的高 32 位与 低 32 位按位异或。

理解上述的转换规则后,你就会明白,为什么 UInt.max.hashValue == -1.hashValue 的结果会为 true 了。这引出了一个很重要的原则:不要直接使用哈希值比较两个不同类型的整数,而是应该将它们转换为同一个类型(并且目标类型的位数不能小于它们的位数)再来进行比较,否则的话会出现不可预料的结果。

RandomAccessIndexType 协议

这个协议是实现“索引 (Index)” 功能的协议,索引可以任意地进行位置偏移、也可以获取可用值的距离。

这个协议所需要实现的方法很简单,分别就是前驱 (predecessor)、后继 (successor)、距离 (distanceTo)、行进 (advancedBy)。这也是我们在使用索引过程中很常见的方法了。

@_transparent public
func successor() -> ${Self} {
  return self &+ 1
}

@_transparent public
func predecessor() -> ${Self} {
  return self &- 1
}

@_transparent public
func distanceTo(other: ${Self}) -> ${Self}.Distance {
  return numericCast((numericCast(other) as IntMax) &- numericCast(self))
}

@_transparent public
func advancedBy(n: ${Self}.Distance) -> ${Self} {
  return numericCast((numericCast(self) as IntMax) &+ numericCast(n))
}

方法清晰明了,简单移动,但是我们还是要说明一些关键点:

&+&- 运算符的作用和用法和正常的 +- 运算符的用法基本相同,只不过它们不会进行溢出检查:

let int1 = Int.max + 1 // 报错,上溢
let int2 = Int.max &+ 1 // 正常输出,输出值和 Int.min 相同

因此我们可以发现,对于作为索引的整数值来说,它们构成了一个环状的结构。

至于 numericCast 函数来说,它的定义如下:

public func numericCast<T : _SignedIntegerType, U :_SignedIntegerType>(x: T) -> U {
  return U(x.toIntMax())
}

可以看出,它可以将传入的 T 参数转换为 IntMax 类型,然后再转换为相应的 U 泛型,这个泛型是根据需要该值的类型而确定的。这样就完成了对应类型的转换和计算。之所以要写得那么复杂,是需要通过 as IntMax 来给这个泛型函数完成 U 泛型的类型确定。这也被称为所谓的上下文推导 (contextually-deduced)。

注意:这个函数实际上有四种类型,区别是它们泛型的参数不同,分别为有符号整数类型 (_SignedIntegerType) 和无符号整数类型 (UnsignedIntegerType),但是它们的实现都是类似的。

BitwiseOperationsType 协议

这个协议需要实现的除了运算符之外,就是一个零值属性了:

public static var allZeros: ${Self} { return 0 }

运算符我们在下面统一进行介绍。

运算符

整数操作中,最为重要的就是运算了。总体而言,在 Swift 中运算符操作的具体实现基本都是由 C++ 写成的,分别封装成了一个函数,放在内置库中供内部调用。

使用广泛的加减乘除余运算

这是整数运算操作中,最广泛的五种运算操作,并且它们的使用量非常大,很多时候借助它们就可以完成我们的需求了。运算符的定义如下:

% for op,method in ('+','add'), ('*','mul'), ('-','sub'):
@_transparent
public func ${op} (lhs: ${Self}, rhs: ${Self}) -> ${Self} {
  let (result, error) = Builtin.${sign}${method}_with_overflow_${BuiltinName}(
    lhs._value, rhs._value, true._value)
  Builtin.condfail(error)
  return ${Self}(result)
}
% end

% for op,inst in [('/', 'div'), ('%', 'rem')]:
@_transparent
public func ${op}(lhs: ${Self}, rhs: ${Self}) -> ${Self} {
  Builtin.condfail((rhs == 0)._value)
% if signed:
  Builtin.condfail(((lhs == ${Self}.min) && (rhs == -1))._value)
% end
  let tmp = Builtin.${sign}${inst}_${BuiltinName}(lhs._value, rhs._value)
  return ${Self}(tmp)
}
%end

主要是借助了内置的 ${sign}${op}_with_overflow_${BuiltinName}()这个函数来实现的操作运算,这个函数还带有溢出检测功能。对于除和余操作来说,由于检测溢出操作还存有 BUG,因此这里只进行了简单的规则判断,溢出检查并没有完全实现。

带有溢出提示的运算

通常情况下,无论是普通的运算符,还是允许溢出的运算符,你都无法知道有没有发生溢出这回事。而在某些需求当中,发生溢出往往需要做一些特别的事情,因此能够对发生溢出与否进行提示的运算方法就应运而生了。

public static func addWithOverflow(lhs: Int, _ rhs: Int) -> (Int, overflow: Bool)
public static func subtractWithOverflow(lhs: Int, _ rhs: Int) -> (Int, overflow: Bool)
public static func multiplyWithOverflow(lhs: Int, _ rhs: Int) -> (Int, overflow: Bool)
public static func divideWithOverflow(lhs: Int, _ rhs: Int) -> (Int, overflow: Bool)
public static func remainderWithOverflow(lhs: Int, _ rhs: Int) -> (Int, overflow: Bool)

实现的方法和上面的方法基本类似,都是借助上述的那个函数完成的,只不过这个函数还可以接收第三个参数,从而返回一个 ({Self}, Builtin.Int1) 类型的闭包。这个闭包的第二个元素就是标记溢出是否发生的标识。

其余运算符

整数运算中还有诸如与、或、非、异或之类的运算,它们的调用方法基本都大同小异:

public func ${op} (lhs: ${Self}, rhs: ${Self}) -> ${Self} {
  return ${Self}(Builtin.${name}_${BuiltinName}(lhs._value, rhs._value))
}

这里,op 指的是运算符的符号,name 指的是对应运算符的缩略名,用以调用对应的函数。

对于 <<>> 左移和右移运算符来说,还对移动的范围做了检测。右操作数的大小必须要小于当前整数类型所占的内存空间,不然的话就会报错。

此外,还有复合赋值运算符的实现,实现非常简单:

@_transparent
public func ${op}=(inout lhs: ${Self}, rhs: ${Self}) {
  lhs = lhs ${op} rhs
}

然后就是自增自减运算符了,不过这个运算符马上就要在 Swift 3 中被移除了:

@_transparent
public prefix func ++ (inout x: ${Self}) -> ${Self} {
  x = x + 1
  return x
}

一些全局函数

除此之外,这个整数类型中还定义了一些全局的函数:

@_transparent
@warn_unused_result
public func _assumeNonNegative(x: ${Self}) -> ${Self} {
  _sanityCheck(x >= 0)
  return ${Self}(Builtin.assumeNonNegative_${BuiltinName}(x._value))
}

% fixedBitWidths = [2**x for x in range(3, 8) if 2**x <= 2 * word_bits]
% for bits in fixedBitWidths:
@_transparent
public func _leadingZeros(x: Builtin.Int${bits}) -> Builtin.Int${bits} {
  return Builtin.int_ctlz_Int${bits}(x, true._value)
}
% end

第一个函数是用来进行负值检测的,它接受一个整数值,如果该值是正数的话,那么就原样返回。而如果该值是负数的话,那么就会报错。

好玩的是,这个函数也是可以用的,虽然没有代码提示:

_assumeNonNegative(1)  // 输出 1
_assumeNonNegative(-1)  // 报错,提示 Assumed non-negative value '-1' is negative

第二个函数从字面意思上,我们可以发现它是用于“前导零 (leading zero)” 这个概念的。前导零,换句话说也称之为数前加零,最为常见的用法就是在日期的书写当中,很多人喜欢写为 "2016-01-01",这个 0 就是前导零的概念。

这个函数主要是用来计算前导零的个数的,我们知道,存储区域中如果没有填充满的地方都会被自动用 0 填充,也就是被前导零填充。因此,计算前导零的操作也就出现了。这里使用的同样是一个内置的函数,返回的前导零个数将通过相同类型返回。

总结

到了这里,我们的整数类型探索就算告一段落了,我们也看完了这一整个文件,然而关于整数类型还有其他的内容,在此我们就不再叙述了。在之后的内容中有空,我们还会继续来看。

在本文中,我们介绍了以下几点内容:

  • gyb文件的定义
  • 一些简单常见的 Python 语法
  • Swift 中整数类型的定义
  • 字节序
  • 最大值和最小值
  • 获取整数位数和字节数
  • 整数类型之间的相互转换
  • 整数中实现的各种各样的协议
  • 对整数进行运算的操作符
  • 一些全局函数

此外,我们还探索到了以下几个没有代码提示,但是却可以使用的函数或属性:

let wordValue = 1._builtinWordValue // 输出 `Word` 类型,一个不透明的类型,我们无法查看其值
Int._sizeInBits  // 输出 64
_assumeNonNegative(1)  // 输出 1

至此,这就是我们所看到的 Swift 整数类型的基础部分内容了,这是一个非常常用的类型,虽然我们很容易忽视它,但是它的功能确实十分强大。