29.1. 位运算符

位运算符可以操作数据结构中每个独立的比特位。它们通常被用在底层开发中,比如图形编程和创建设备驱动。位运算符在处理外部资源的原始数据时也十分有用,比如对自定义通信协议传输的数据进行编码和解码。

Swift 支持 C 语言中的全部位运算符,接下来会一一介绍。

Bitwise NOT Operator(按位取反运算符)

按位取反运算符(~)对一个数值的全部比特位进行取反:

按位取反运算符是一个前缀运算符,直接放在运算数之前,并且它们之间不能添加任何空格:

let initialBits: UInt8 = 0b00001111
let invertedBits = ~initialBits // 等于 0b11110000

UInt8 类型的整数有 8 个比特位,可以存储 0 ~ 255 之间的任意整数。这个例子初始化了一个 UInt8 类型的整数,并赋值为二进制的 00001111,它的前 4 位为 0,后 4 位为 1。这个值等价于十进制的 15

接着使用按位取反运算符创建了一个名为 invertedBits 的常量,这个常量的值与全部位取反后的 initialBits 相等。即所有的 0 都变成了 1,同时所有的 1 都变成 0invertedBits 的二进制值为 11110000,等价于无符号十进制数的 240

Bitwise AND Operator(按位与运算符)

按位与运算符(& 对两个数的比特位进行合并。它返回一个新的数,只有当两个数的对应位1 的时候,新数的对应位才为 1

在下面的示例当中,firstSixBitslastSixBits 中间 4 个位的值都为 1。使用按位与运算符之后,得到二进制数值 00111100,等价于无符号十进制数的 60

let firstSixBits: UInt8 = 0b11111100
let lastSixBits: UInt8  = 0b00111111
let middleFourBits = firstSixBits & lastSixBits // 等于 00111100

Bitwise OR Operator(按位或运算符)

按位或运算符(|可以对两个数的比特位进行比较。它返回一个新的数,只要两个数的对应位中有任意一个1 时,新数的对应位就为 1

在下面的示例中,someBitsmoreBits 存在不同的位被设置为 1。使用按位或运算符之后,得到二进制数值 11111110,等价于无符号十进制数的 254

let someBits: UInt8 = 0b10110010
let moreBits: UInt8 = 0b01011110
let combinedbits = someBits | moreBits // 等于 11111110

Bitwise XOR Operator(按位异或运算符)

按位异或运算符,或称“排外的或运算符”(^),可以对两个数的比特位进行比较。它返回一个新的数,当两个数的对应位不相同时,新数的对应位就为 1,并且对应位相同时则为 0

在下面的示例当中,firstBitsotherBits 都有一个自己为 1,而对方为 0 的位。按位异或运算符将新数的这两个位都设置为 1。在其余的位上 firstBitsotherBits 是相同的,所以设置为 0

let firstBits: UInt8 = 0b00010100
let otherBits: UInt8 = 0b00000101
let outputBits = firstBits ^ otherBits // 等于 00010001

Bitwise Left and Right Shift Operators(按位左移、右移运算符)

*按位左移运算符(>)*可以对一个数的所有位进行指定位数的左移和右移,但是需要遵守下面定义的规则。

对一个数进行按位左移或按位右移,相当于对这个数进行乘以 2 或除以 2 的运算。将一个整数左移一位,等价于将这个数乘以 2,同样地,将一个整数右移一位,等价于将这个数除以 2。

无符号整数的移位运算

对无符号整数进行移位的规则如下:

  1. 已存在的位按指定的位数进行左移和右移。
  2. 任何因移动而超出整型存储范围的位都会被丢弃。
  3. 0 来填充移位后产生的空白位。

这种方法称为逻辑移位

以下这张图展示了 11111111 > 1(即把 11111111 向右移动 1 位)的结果。蓝色的数字是被移位的,灰色的数字是被抛弃的,橙色的 0 则是被填充进来的:

下面的代码演示了 Swift 中的移位运算:

let shiftBits: UInt8 = 4 // 即二进制的 00000100
shiftBits << 1           // 00001000
shiftBits << 2           // 00010000
shiftBits << 5           // 10000000
shiftBits << 6           // 00000000
shiftBits >> 2           // 00000001

可以使用移位运算对其他的数据类型进行编码和解码:

let pink: UInt32 = 0xCC6699
let redComponent = (pink & 0xFF0000) >> 16  // redComponent 是 0xCC,即 204
let greenComponent = (pink & 0x00FF00) >> 8 // greenComponent 是 0x66, 即 102
let blueComponent = pink & 0x0000FF         // blueComponent 是 0x99,即 153

这个示例使用了一个命名为 pinkUInt32 型常量来存储 Cascading Style Sheets(CSS)中粉色的颜色值。该 CSS 的颜色值 #CC6699,在 Swift 中表示为十六进制的 0xCC6699。然后利用按位与运算符(&)和按位右移运算符(>>)从这个颜色值中分解出红(CC)、绿(66)以及蓝(99)三个部分。

红色部分是通过对 0xCC66990xFF0000 进行按位与运算后得到的。0xFF0000 中的 0 部分“掩盖”了 OxCC6699 中的第二、第三个字节,使得数值中的 6699 被忽略,只留下 0xCC0000

然后,将这个数向右移动 16 位(>> 16)。十六进制中每两个字符占用 8 个比特位,所以移动 16 位后 0xCC0000 就变为 0x0000CC。这个数和 0xCC 是等同的,也就是十进制数值的 204

同样的,绿色部分通过对 0xCC66990x00FF00 进行按位与运算得到 0x006600。然后将这个数向右移动 8 位,得到 0x66,也就是十进制数值的 102

最后,蓝色部分通过对 0xCC66990x0000FF 进行按位与运算得到 0x000099。因为 0x000099 已经等于 0x99 ,也就是十进制数值的 153,所以这个值就不需要再向右移位了。

有符号整数的移位运算

对比无符号整数,有符号整数的移位运算相对复杂得多,这种复杂性源于有符号整数的二进制表现形式。(为了简单起见,以下的示例都是基于 8 比特的有符号整数,但是其中的原理对任何位数的有符号整数都是通用的。)

有符号整数使用第 1 个比特位(通常被称为符号位)来表示这个数的正负。符号位为 0 代表正数,为 1 代表负数。

其余的比特位(通常被称为数值位)存储了实际的值。有符号正整数和无符号数的存储方式是一样的,都是从 0 开始算起。这是值为 4Int8 型整数的二进制位表现形式:

符号位为 0(代表这是一个“正数”),另外 7 位则代表了十进制数值 4 的二进制表示。

负数的存储方式略有不同。它存储 2n 次方减去其实际值的绝对值,这里的 n 是数值位的位数。一个 8 比特位的数有 7 个比特位是数值位,所以是 27 次方,即 128

这是值为 -4Int8 型整数的二进制表现形式:

这次的符号位为 1,说明这是一个负数,另外 7 个位则代表了数值 124(即 128 - 4)的二进制表示:

负数的表示通常被称为二进制补码。用这种方法来表示负数乍看起来有点奇怪,但它有几个优点。

首先,如果想对 -1-4 进行加法运算,我们只需要对这两个数的全部 8 个比特位执行标准的二进制相加(包括符号位),并且将计算结果中超出 8 位的数值丢弃:

其次,使用二进制补码可以使负数的按位左移和右移运算得到跟正数同样的效果,即每向左移一位就将自身的数值乘以 2,每向右一位就将自身的数值除以 2。要达到此目的,对有符号整数的右移有一个额外的规则:当对有符号整数进行按位右移运算时,遵循与无符号整数相同的规则,但是对于移位产生的空白位使用符号位进行填充,而不是用 0

这个行为可以确保有符号整数的符号位不会因为右移运算而改变,这通常被称为算术移位

由于正数和负数的特殊存储方式,在对它们进行右移的时候,会使它们越来越接近 0。在移位的过程中保持符号位不变,意味着负整数在接近 0 的过程中会一直保持为负。