Swift学习笔记之高级运算符
 visitors

除了基本运算符外,Swift还提供了许多可以对数值进行复杂运算的高级运算符。这些高级运算符包含了在C和objc中已经被大家熟知的位运算符和位移运算符。

与C语言和objc的算数运算符不同,Swift中的算数运算符默认是不会溢出的。所有溢出行为都会被捕获并报告位错误。如果想让系统允许溢出行为,可以选择使用Swift中另一套默认支持溢出的运算符,比如溢出运算符(&+),所有的溢出运算符都是以&开头的。

位运算符

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

按位取反运算符

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

按位取反运算符是一个前缀运算符,需要直接放在运算的数之前,并且它们之间不能加空格。

1
2
let initialBits: UInt8 = 0b00001111   //十进制等于15
let invertedBits = ~initialBits //等于 0b11110000,十进制为240

按位与运算符

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

在下面的示例当中,firstBitslastBits 中间 4 个位的值都为 1。按位与运算符对它们进行了运算,得到二进制数值 00111100,等价于无符号十进制数的 60:

1
2
3
let firstBits: UInt8 = 0b11111100
let lastBits: UInt8 = 0b00111111
let middleBits = firstBits & lastBits // 等于 00111100

按位或运算符

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

在下面的示例中,someBitsmoreBits 不同的位会被设置为 1。接位或运算符对它们进行了运算,得到二进制数值 11111110,等价于无符号十进制数的 254

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

按位异或运算符

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

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

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

按位左移、右移运算符

按位左移运算符(<<)和按位右移运算符(>>)可以对一个数的所有位进行指定位数的左移和右移。对一个数的左移和右移一位相当于对一个数乘以2或者除以2。

无符号整数的位移运算

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

  • 已经存在的位按指定的位数进行左移和右移
  • 任何因移动超出整型存储范围的位都会被丢弃
  • 0来填充位移后产生的空白
    这种方法称为逻辑位移。

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

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

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

还可以使用位移运算对其它数据类型进行编码和解码。

有符号整数的位移运算

对比无符号整数,有符号整数的位移运算相对就要复杂得多,这种复杂性源于有符号整数的二进制表现新式。
有符号整数使用第一个比特位(通常称为符号位)来表示这个数的正负。符号为0代表正数,为1代表负数。其余的比特位(通常称为数值位)存储了实际的值。

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

负数的存储方式略有不同。它存储的值的绝对值等于 2n 次方减去它的实际值(也就是数值位表示的值),这里的 n 为数值位的比特位数。一个 8 比特位的数有 7 个比特位是数值位,所以是 27 次方,即 128
这是值为 -4Int8 型整数的二进制位表现形式:

这次的符号位为 1,说明这是一个负数,另外 7 个位则代表了数值 124(即 128 - 4)的二进制表示。负数的表示通常被称为二进制补码表示,用这种方法表示负数乍看起来有点奇怪,但它有一下几个优点。

  • 如果想对-1-4进行加法运算,我们只需要将这两个数的全部8个比特位相加,并且将计算结果中超出8位的值丢弃,其次,使用二进制补码可以使负数的按位左移和右移运算得到跟正数同样的效果,即每向左移一位就将自身的数值乘以 2,每向右一位就将自身的数值除以 2。要达到此目的,对有符号整数的右移有一个额外的规则:

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

    这个行为可以确保有符号整数的符号位不会因为右移运算而改变,这通常被称为算术移位
    由于正数和负数的特殊存储方式,在对它们进行右移的时候,会使它们越来越接近 0。在移位的过程中保持符号位不变,意味着负整数在接近 0 的过程中会一直保持为负。

溢出运算符

在默认情况下,当向一个整数赋予超过它容量的值时,Swift默认会报错,而不是产生一个无效的数。这个行为为我们在运算过大或者过小的数的时候提供了额外的安全性。
例如Int16型整数能容纳的有符号整数范围是-3276832767,当一个为Int16型变量的值超出了这个范围时,系统会报错:

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

溢出运算符可以让数值溢出的时候采取截断处理,而非报错。可以使用Swift提供的三个溢出运算符来让系统支持整数溢出运算。这些运算符都是以&开头的:

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

无符号数值上溢

数值有可能出现上溢或者下溢。
这个示例演示了但我们对一个无符号整数使用溢出加法(&+)进行上溢运算时会发生什么:

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

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

无符号数值下溢

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

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

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

有符号数值溢出

溢出也会发生在有符号整型数值上。在对有符号整型数值进行溢出加法或溢出减法运算时,符号位也需要参与计算。比如下面的例子:

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

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

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

运算符函数

类和结构体可以为现有的运算符提供自定义的实现,这通常被称为运算符重载。

下面一个例子展示了如何为自定义的结构体实现加法运算符(+)。算数运算符是一个双目运算符,因为它可以对两个值进行运算,同时它还是中缀运算符,因为它出现在两个值中间。

例子中定义了一个名为 Vector2D 的结构体用来表示二维坐标向量 (x, y),紧接着定义了一个可以对两个 Vector2D 结构体进行相加的运算符函数:

1
2
3
4
5
6
struct Vector2D {
var x = 0.0, y = 0.0
}
func + (left: Vector2D, right: Vector2D) -> Vector2D {
return Vector2D(x: left.x + right.x, y: left.y + right.y)
}

该运算符函数被定义为一个全局函数,并且函数的名字与它要进行重载的 + 名字一致。因为算术加法运算符是双目运算符,所以这个运算符函数接收两个类型为 Vector2D 的参数,同时有一个 Vector2D 类型的返回值。

在这个实现中,输入参数分别被命名为 leftright,代表在 + 运算符左边和右边的两个 Vector2D 实例。函数返回了一个新的 Vector2D 实例,这个实例的 xy 分别等于作为参数的两个实例的 xy 的值之和。

这个函数被定义成全局的,而不是 Vector2D 结构体的成员方法,所以任意两个 Vector2D 实例都可以使用这个中缀运算符:

1
2
3
4
let vector = Vector2D(x: 3.0, y: 1.0)
let anotherVector = Vector2D(x: 2.0, y: 4.0)
let combinedVector = vector + anotherVector
// combinedVector 是一个新的 Vector2D 实例,值为 (5.0, 5.0)

前缀和后缀运算符

上个例子演示了一个双目中缀运算符的自定义实现。类与结构体也能提供标准单目运算符的实现。单目运算符只运算一个值。当运算符出现在值之前时,它就是前缀的(例如 -a),而当它出现在值之后时,它就是后缀的(例如 b!)。

要实现前缀或者后缀运算符,需要在声明运算符函数的时候在func关键字之前指定prefix或者postfix修饰符。

1
2
3
prefix func - (vector: Vector2D) -> Vector2D {
return Vector2D(x: -vector.x, y: -vector.y)
}

这段代码为 Vector2D 类型实现了单目负号运算符。由于该运算符是前缀运算符,所以这个函数需要加上 prefix 修饰符。

对于简单数值,单目负号运算符可以对它们的正负性进行改变。对于 Vector2D 来说,该运算将其 xy 属性的正负性都进行了改变:

1
2
3
4
5
let positive = Vector2D(x: 3.0, y: 4.0)
let negative = -positive
// negative 是一个值为 (-3.0, -4.0) 的 Vector2D 实例
let alsoPositive = -negative
// alsoPositive 是一个值为 (3.0, 4.0) 的 Vector2D 实例

复合赋值运算符

复合赋值运算符将赋值运算符(=)与其它运算符进行结合。例如,将加法与赋值结合成加法赋值运算符(+=)。在实现的时候,需要把运算符的左参数设置成 inout 类型,因为这个参数的值会在运算符函数内直接被修改。

1
2
3
func += (inout left: Vector2D, right: Vector2D) {
left = left + right
}

因为加法运算在之前已经定义过了,所以在这里无需重新定义。在这里可以直接利用现有的加法运算符函数,用它来对左值和右值进行相加,并再次赋值给左值:

1
2
3
4
var original = Vector2D(x: 1.0, y: 2.0)
let vectorToAdd = Vector2D(x: 3.0, y: 4.0)
original += vectorToAdd
// original 的值现在为 (4.0, 6.0)

注:不能对默认的赋值运算符(=)进行重载。只有组合赋值运算符可以被重载。同样地,也无法对三目条件运算符 (a ? b : c) 进行重载。

等价运算符

自定义的类和结构体没有对等价运算符进行默认实现,等价运算符通常被称为“相等”运算符(==)与“不等”运算符(!=)。对于自定义类型,Swift 无法判断其是否“相等”,因为“相等”的含义取决于这些自定义类型在你的代码中所扮演的角色。

为了使用等价运算符能对自定义的类型进行判等运算,需要为其提供自定义实现,实现的方法与其它中缀运算符一样:

1
2
3
4
5
6
func == (left: Vector2D, right: Vector2D) -> Bool {
return (left.x == right.x) && (left.y == right.y)
}
func != (left: Vector2D, right: Vector2D) -> Bool {
return !(left == right)
}

自定义运算符

除了实现标准运算符,在 Swift 中还可以声明和实现自定义运算符。
新的运算符要使用 operator 关键字在全局作用域内进行定义,同时还要指定 prefixinfix 或者 postfix 修饰符:

1
prefix operator +++ {}

上面的代码定义了一个新的名为 +++ 的前缀运算符。对于这个运算符,在 Swift 中并没有意义,因此我们针对 Vector2D 的实例来定义它的意义。对这个示例来讲,+++ 被实现为“前缀双自增”运算符。它使用了前面定义的复合加法运算符来让矩阵对自身进行相加,从而让 Vector2D 实例的 x 属性和 y 属性的值翻倍:

1
2
3
4
prefix func +++ (inout vector: Vector2D) -> Vector2D {
vector += vector
return vector
}