2016-07-14 07:24:07 yangmeng13930719363 阅读数 2445

高级运算符

文档地址

作为 基本运算符 的补充,Swift 提供了几个高级运算符执行对数传值进行更加复杂的操作。这样写运算包括所有你从 C 或 Objective-C 所熟悉的按位操作和移位运算符。

与 C 的算术运算符不同,Swift 中算术运算符默认是不会溢出的。溢出行为都会作为错误被捕获。为了允许溢出行为,可以使用 Swift 中另一套默认支持的溢出运算符,比如溢出加法运算符(&+)。所有这些溢出运算符都是以 & 符号开始的。

当你定义了你自己的结构体,类以及枚举的时候,那么为这些自定义类型也提供 Swift 标准的运算符将会有用的。Swift 简化了这些运算符的定制实现,并且精确地确定了你创建的每个类型的运算符所具有的行为。

你不会被预定义的运算符所限制。在 Swift 中你可以自由地定义你自己的中缀,前缀,后缀和赋值运算符,以及相对应的优先级和结合性。这些运算符可以像预先定义的运算符一样在你的代码里使用,甚至你可以扩展已存在的类型来支持你自己定义的运算符。

按位运算符

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

Swift 支持 C 里面所有的按位运算符,具体如下:

按位取反运算符

按位取反运算符(~)是对所有位的数字进行取反操作:

bitwiseNOT_2x

按位取反运算符是一个前缀运算符,需要直接放在运算符的前面,并且不能有空格:

let initialBits: UInt8 = 0b00001111
let invertedBits = ~initialBits  // equals 11110000

UInt8 类型的整数有八位,可以存储 0255 之间的任意值。这个例子初始化了一个 UInt8 类型的整数,二进制为 00001111,前四位全是 0,后四位都是 1。这和十进制的 15 是相等的。

然后使用取反运算符创建一个新的常量名为 invertedBits,它和 initialBits 相等,但是所有位都被取反了。0 变为了 11 变为了 0invertedBits 的值是 11110000,和十进制的无符号整数 240 相等。

按位与运算符

按位与运算符(&)可以对两个数的比特位进行合并。它会返回一个新的数,只有当这两个数都是 1 的时候才能返回 1

bitwiseAND_2x

在下面的例子中,firstSixBitslastSixBits 的中间四个位都为 1。按位与可以把它们合并为一个新值 00111100,对应十进制的值为 60

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

按位或运算符

按位或运算符(|)可以对两个比特位进行比较,然后返回一个新数,只要两个操作位任意一个为 1 时,那么对应的位数都为 1

bitwiseOR_2x

在下面的例子中,someBitsmoreBits在不同的位设置了 1。按位或运算符把它们合并为 11111110,对应的十进制是无符号整数 154

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

按位异或运算符

按位异或运算符(^)可以对两个数的比特位进行比较。它返回一个新的数,当两个操作数的对应位不相同时,该数的对应位就为 1

bitwiseXOR_2x

在下面的例子中,firstBitsotherBits 的值有一位设置设置为 1,而对方设置为 0。按位异或运算符会将这两个位上的值设置为 1firstBitsotherBits 其他位都设置为了 0

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

按位左移或右移运算符

按位左移(<<)或右移(>>)运算符可以把所有位数的数字向左或向右移动一个确定的位数,但是需要遵守下面定义的规则。

按位左移或右移具有乘以 2 或除以 2 的效果。将一个数左移一位相当于把这个数乘以 2,将一个数右移一位相当于把这个数除以 2

无符号整数的移位操作

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

  1. 已经存在的比特位按指定的位数进行左移和右移。

  2. 任何移动超出整型存储边界的位都会被丢弃。

  3. 0 来填充向左或向右移动后产生的空白位。

这种方法称之为 逻辑操作

下图展示了 11111111 << 1(即把 11111111 向左移动 1 位),11111111 >> 1(即把 11111111 向右移动 1 位),蓝色的数字是被移位的,灰色的数字被舍弃,橙色的数字 0 是新插入的:

bitshiftUnsigned_2x

下面的代码展示了 Swift 的移位操作:

let shiftBits: UInt8 = 4   // 00000100 in binary
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 is 0xCC, or 204
let greenComponent = (pink & 0x00FF00) >> 8   // greenComponent is 0x66, or 102
let blueComponent = pink & 0x0000FF           // blueComponent is 0x99, or 153

这个示例使用了一个命名为 pinkUInt32 型常量来存储层叠样式表(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。并且不需要进行向右移位,所以结果为 0x99,也就是十进制数值的 153

有符号整型的位移操作

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

有符号整型使用第一位(称作符号位)表示这个整数是正数还是负数。符号位为 0 表示为正数, 1 表示为负数。

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

bitshiftSignedFour_2x

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

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

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

bitshiftSignedMinusFour_2x

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

bitshiftSignedMinusFourValue_2x

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

首先,如果想对 1-4 进行加法操作,我们只需要将这两个数的全部 8 个比特位进行相加,并且将计算结果中超出 8 位的数值丢弃:

bitshiftSignedAddition_2x

其次,使用二进制补码可以使负数的按位左移和右移操作得到跟正数同样的效果,即每向左移一位就将自身的数值乘以 2,每向右一位就将自身的数值除以 2。要达到此目的,对有符号整数的右移有一个额外的规则:

  • 当对正整数进行按位右移操作时,遵循与无符号整数相同的规则,但是对于移位产生的空白位使用符号位进行填充,而不是用 0

bitshiftSigned_2x

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

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

溢出运算符

在默认情况下,当向一个整数赋超过它容量的值时,Swift 默认会报错,而不是生成一个无效的数。这个行为给我们操作过大或着过小的数的时候提供了额外的安全性。

例如,Int16 型整数能容纳的有符号整数范围是 -3276832767,当为一个 Int16 型变量赋的值超过这个范围时,系统就会报错:

var potentialOverflow = Int16.max
// potentialOverflow 的值是 32767, 这是 Int16 能容纳的最大整数
potentialOverflow += 1
// 这里会报错

为过大或者过小的数值提供错误处理,能让我们在处理边界值时更加灵活。

然而,也可以选择让系统在数值溢出的时候采取截断操作,而非报错。可以使用 Swift 提供的三个溢出操作符(overflow operators)来让系统支持整数溢出运算。这些操作符都是以 & 开头的:

  • 溢出加法 &+
  • 溢出减法 &-
  • 溢出乘法 &*

数值溢出

数值可能出现向上溢出或向下溢出。

这个示例演示了当对一个无符号整数使用溢出加法(&+)进行上溢运算时会发生什么:

var unsignedOverflow = UInt8.max
// unsignedOverflow 等于 UInt8 所能容纳的最大整数 255
unsignedOverflow = unsignedOverflow &+ 1
// 此时 unsignedOverflow 等于 0

unsignedOverflow 被初始化为 UInt8 所能容纳的最大整数(255,二进制为 11111111)。溢出加法运算符(&+)对其进行加 1 操作。这使得它的二进制表示正好超出 UInt8 所能容纳的位数,也就导致了数值的溢出,如下图所示。数值溢出后,留在 UInt8 边界内的值是 00000000,也就是十进制数值的 0

overflowAddition_2x

同样地,当我们对一个无符号整数使用溢出减法(&-)进行下溢运算时也会产生类似的现象:

var unsignedOverflow = UInt8.min
// unsignedOverflow 等于 UInt8 所能容纳的最小整数 0
unsignedOverflow = unsignedOverflow &- 1
// 此时 unsignedOverflow 等于 255

UInt8 型整数能容纳的最小值是 0,以二进制表示即 00000000。当使用溢出减法运算符(&-)对其进行减 1 操作时,数值会产生下溢并被截断为 11111111, 也就是十进制数值的 255

overflowUnsignedSubtraction_2x

溢出也会发生在有符号整型数值上。在对有符号整型数值进行溢出加法或溢出减法运算时,符号位也需要参与计算,正如按位左移/右移运算符所描述的。

var signedOverflow = Int8.min
// signedOverflow 等于 Int8 所能容纳的最小整数 -128
signedOverflow = signedOverflow &- 1
// 此时 signedOverflow 等于 127

Int8 型整数能容纳的最小值是 -128,以二进制表示即 10000000。当使用溢出减法操作符对其进行减 1 操作时,符号位被翻转,得到二进制数值 01111111,也就是十进制数值的 127,这个值也是 Int8 型整数所能容纳的最大值。

overflowSignedSubtraction_2x

对于无符号与有符号整型数值来说,当出现上溢时,它们会从数值所能容纳的最大数变成最小的数。同样地,当发生下溢时,它们会从所能容纳的最小数变成最大的数。

优先级和结合性

运算符的优先级(precedence) 使得一些运算符优先于其他运算符,高优先级的运算符会先被计算。

结合性(associativity)定义了具有相同优先级的运算符是如何结合(或关联)的 —— 是与左边结合为一组,还是与右边结合为一组。可以这样理解:『它们是与左边的表达式结合的』或者『它们是与右边的表达式结合的』。

在复合表达式的运算顺序中,运算符的优先级和结合性是非常重要的。举例来说,为什么下面这个表达式的运算结果是 17

2 + 3 % 4 * 5
// = 17

如果严格地从左到右进行运算,则运算的过程是这样的:

  • 2 + 3 = 5
  • 5 % 4 = 1
  • 1 * 5 = 5

然而正确的答案是 17,而不是5。优先级高的运算符要先于优先级低的运算符进行计算。与 C 语言类似,在 Swift 中,取余运算符(%)和乘法运算符(*)的优先级高于加法运算符(+)。因此,它们的计算顺序要先于加法运算。

但是,取余和乘法具有相同的优先级。这时为了得到正确的运算顺序,还需要考虑结合性,乘法与取余运算都是左结合的。可以将这考虑成为这两部分表达式都隐式地加上了括号:

2 + ((3 % 4) * 5)

(3 % 4) 是 3,所以表达式等价于:

2 + (3 * 5)

(3 * 5) 是 15,所以表达式等价于:

2 + 15

此时可以容易地看出计算的结果为 17

如果想查看完整的 Swift 运算符优先级和结合性规则,请参考表达式。以及 Swift 标准库中的运算符

注意:
对于C语言和 Objective-C 来说,Swift 的运算符优先级和结合性规则是更加简洁和可预测的。但是,这也意味着它们于那些基于C的语言不是完全一致的。在对现有的代码进行移植的时候,要注意确保运算符的行为仍然是按照你所想的那样去执行。

运算符函数

类和结构体可以为现有的操作符提供自定义的