高级运算符
除了 基础运算符 外,Swift 还提供了一些高级运算符来执行更复杂的运算。包括你熟悉的 C 和 Objective-C 中所有的按位运算符和位移运算符。
与 C 语言中的算术运算符不同的是,Swift 中的算术运算符默认不会溢出。溢出被捕获后是作为错误进行处理的。如果想使用溢出行为,请使用 Swift 的第二种算术运算符集,比如加号溢出操作符( &+
)。所有的溢出运算符都以 ( &
) 符开头。
当你定义自己的结构体、类、枚举时,对于这种自定义的数据类型提供你自己的基于标准的 Swift 运算符的实现是很有用的。Swift 让他变得简单:给每一个你自定义的数据类型提供一具体的实现来精确控制他们的行为。
你不仅限于预定义的的操作符。Swift 允许你自定义中缀、前缀、后缀,和赋值运算符,来自定义优先级和关联值。这些操作符可以像任意的预定义操作符一样在你的代码中使用,你甚至可以扩展已经存在的类型类支持你自定义的运算符。
按位运算符
按位运算符 使你可以操作数据结构中各个原始数据位。它们通常用于底层编程,比如图像编程和设备驱动的创建。当你操作外部数据源的原始数据时,按位运算符也很有用,比如通过自定义协议进行通信数据的编码和解码。
如下所述,Swift 支持 C 中的所有的按位运算符。
按位取反运算符
按位取反运算符 (~
) 会反转数字中所有的二进制数:
按位取反运算符是一个前缀运算符,应该直接放在被他操作的数字前面,中间没有空格:
let initialBits: UInt8 = 0b00001111
let invertedBits = ~initialBits // 等于 11110000
UInt8
类型的整数有8个二进制数,可以存储 0
到 255
之间任意一个数字。示例中初始化了一个 UInt8
类型的整数,二进制数为 00001111
,头四个二进制数为 0
,后四个为 1
。相当于十进制数 15
。
然后,用按位取反运算符创建一个新的名为 invertedBits
的常量,等于 initialBits
,但是所有的二进制数都反转了。所有的零变成一,一变成零。 invertedBits
的值为 11110000
,相当于无符号十进制数 240
。
按位与运算符
按位与运算符 (&
) 对两个数按位进行合并操作,然后返回了一个新值,只有当这个两个运算数的二进制位 都 为 1
时,新值对应的二进制位才为 1
:
在下面的例子中,常量 firstSixBits
和 lastSixBits
中间的四个二进制位都是 1
。对他们进行按位与运算后,产生了一个新值 00111100
,相当于无符号十进制数 60
:
let firstSixBits: UInt8 = 0b11111100
let lastSixBits: UInt8 = 0b00111111
let middleFourBits = firstSixBits & lastSixBits // 等于 00111100
按位或运算符
按位或运算 (|
) 对两个数的按位进行比较,然后返回了一个新值,只要其中 任意一个 输入数对应的二进制位为 1
,新值对应的二进制位就为 1
:
如下代码,常量 someBits
和 moreBits
的值在不同的二进制位上有 1
。对它们进行按位或运算后,得到一个新值 11111110
,相当于无符号十进制数 254
:
let someBits: UInt8 = 0b10110010
let moreBits: UInt8 = 0b01011110
let combinedbits = someBits | moreBits // 等于 11111110
按位异或运算符
按位异或运算符 ,或者 按位互斥或运算符 将两个数的二进制数进行比较。返回一个新值,当两个操作数对应的二进制数不同时,新值对应的二进制数置为 1
,相同时置为 0
:
下面的例子中,常量 firstBits
和 otherBits
各有一个二进制数为 1
而对方对应的二进制数不是。按位异或运算符将返回值中这两个对应的二进制数都置为了 1
。返回值中其他所有与常量 firstBits
和 otherBits
相匹配的二进制数都被置为了 0
:
let firstBits: UInt8 = 0b00010100
let otherBits: UInt8 = 0b00000101
let outputBits = firstBits ^ otherBits // 等于 00010001
按位左移和右移运算符
按位左移运算符(<<
)与按位右移运算符(>>
)是根据下面定义的规则将一个数所有的二进制数往左或者往右偏移一个确定位移。
按位左移和右移具有将一个整数乘以或者除以因子为二的效果。将一个整数的二进制数左移一位,整数的值会翻一番,而将其右移一位,整数的值则减小一半。
无符号整数的偏移
下面是无符号整数的按位偏移操作:
1.已经存在的二进制数都往左或者往右偏移指定的位数。
2.任意一个被移到该整数存储空间之外的二进制数都会被舍弃。
3.将零插入到原始二进制数左移或者右移之后遗留下来的空格内。
这种方法被称为 逻辑偏移 。
下图所示的是表达式 11111111 << 1
(11111111
左移一位),还有表达式 11111111 >> 1
(11111111
右移一位)的结果。蓝色的数字是被偏移的,灰色的是被舍弃的,橘色的零是被插入的:
在 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
案例中,用一个 UInt32
类型的常量 pink
来存储层叠样式表中的粉色值。这个 CSS 颜色值 #CC6699
在 Swift 的十六进制表示法中写作 0xCC6699
。然后该颜色值通过按位与(&
)和按位右移(>>
)运算,被分解为红色(CC
)、绿色(66
)和蓝色(99
)。
红色成分是通过对 0xCC6699
和 0xFF0000
进行按位与操作获得的。 0xFF0000
中的零有效的「屏蔽」了 0xCC6699
中第二个和第三个字节,舍去 6699
,结果为 0xCC0000
。
然后,将这个结果右移 16 个二进制位(>> 16
)。在十六进制中,每对字符占据 8 个二进制数,所以右移 16 个二进制位会将 0xCC0000
转换为 0x0000CC
。相当于 0xCC
,也就是十进制数 204
。
类似的,绿色成分是通过对 0xCC6699
和 0x00FF00
进行按位与操作获得的,返回值为 0x006600
。将这个返回值往右偏移 8 个二进制位,得到 0x66
,即十进制数 102
。
最后,蓝色成分是通过对 0xCC6699
和 0x0000FF
进行按位与操作获得的,返回值为 0x000099
。不需要再将它往右偏移了,因为 0x000099
本来就等于 0x99
,即十进制数 153
。
有符号整型的偏移
鉴于有符号整型在二进制的表示方式,对其进行按位偏移比无符号整型要复杂。(简单起见,下面的例子是基于 8 位二进制数的有符号整型,但对任意长度的有符号整型的偏移,原理都是一样的。)
有符号整型用第一个二进制位(称作符号位)来表示其实正数还是负数。0
表示正数,1
表示负数。
余下的二进制位(称作数值位)存储真正的数值。正数的存储方式与无符号整型一致,从 0
开始往上计数。正数 4 的二进制位在 Int8
中是这么表示的:
符号位为 0
(意思是「正数」),7 个数值位就是用二进制表示的整数 4
。
不过,负数的存储有所不同。它们是通过用 2
的 n
次幂减去它们的绝对值来存储的,其中 n
就是数值位的位数。一个 8 位二进制数有 7 个数值位,也就是 2
的 7
次幂,即 128
。
整数 -4
的二进制数在 Int8
上是这样表示的:
这一次,符号位是 1
(意思是「负数」),7 个数值位表示十进制数 124
(即 128 - 4
):
这种负数的编码形式称为 二进制补码 。看起来有些不同寻常,但是它有几个优点:
首先,你可以将 -1
加到 -4
上,简单地对 8 个二进制数(包括符号位)进行标准的二进制加法操作,完成后舍弃超出 8 个二进制位的数:
其次,二进制补码也允许你像对正数一样对负数进行向左向右偏移,数值左移翻翻,右移减半。为了实现这种操作,当有符号整型被向右偏移时,需要遵循一个额外的规则:当将有符号整型向右偏移时,遵循的规则与偏移无符号整型一样,但是填补左边空白的二进制位要用 符号位 ,而不是零。
这个操作被称为 算数偏移 ,确保了这个有符号整型在右移之后符号不变。
鉴于正数和负数的特殊存储方式,对其进行右偏移使得它们的值更接近于零。在偏移过程中保持符号位不变,意味着负数还是负数,只是它们的值更接近零了。
溢出运算符
如果你向一个整型的常量或者变量中插入一个它容纳不了的数值,Swift 默认会报错,而不会允许生成一个无效的数。这种处理方式在你操作一个过大或者过小的数时,提供了额外的安全保障。
例如,Int16
整型可以容纳从 -32768
到 32767
之间的有符号整型值。当你试图往一个 Int16
类型的常量或者变量里设置一个超出这个范围的值时,就会导致错误:
var potentialOverflow = Int16.max
// potentialOverflow 等于 32767,是 Int16 能容纳的最大值
potentialOverflow += 1
// 这行代码会导致错误
当数值变得太大或者太小时,提供错误处理方法会让你在编码边界条件时,拥有更多的灵活性:
不过,当你特别期望在出现溢出情况时,能够截取有效的二进制数,你可以选择这个操作而不是抛出一个错误。Swift 为整型计算的溢出操作提供了三个算数 溢出运算符
可供选择。这几个运算符都是以 (&
)开头:
- 溢出加法运算符 (
&+
) - 溢出减法运算符 (
&-
) - 溢出乘法运算符 (
&*
)
值溢出
数字可以向正方向和负方向溢出。
下面的例子描述了,当无符号整型允许正方向溢出时,使用溢出加法运算符(&+
)发生了什么:
var unsignedOverflow = UInt8.max
// unsignedOverflow 等于 255, 是 UInt8 能容纳的最大值
unsignedOverflow = unsignedOverflow &+ 1
// unsignedOverflow 现在等于 0
变量 unsignedOverflow
初始化被赋予 UInt8
可容纳的最大值( 255
, 或者二进制数 11111111
)。然后用溢出加法运算符 (&+
)加 1
。如下图所示,这促使二进制数的表示刚好超出了 UInt8
所能容纳的大小,引起了边界溢出。 溢出加法运算后留在 UInt8
边界里的数值为 00000000
,或者零。
当无符号整型允许负方向溢出时,发生了类似的事情。下面是使用溢出减法运算符 (&-
)的例子:
var unsignedOverflow = UInt8.min
// unsignedOverflow 等于 0, 是 UInt8 能容纳的最小值
unsignedOverflow = unsignedOverflow &- 1
// unsignedOverflow 现在等于 255
UInt8
能容纳的最小值是零,或者二进制数 00000000
。如果你使用溢出减法运算符 (&-
),从 00000000
中减去 1
, 则该数值会溢出并环绕到 11111111
,或者十进制数 255
。
溢出也会出现在有符号整型上,正如在 按位左移与按位右移操作符 中描述的那样,作用于有符号整型的加法和减法是操作在二进制位上的,符号位作为数字的一部分也参与了加法或减法运算。
var signedOverflow = Int8.min
// signedOverflow 等于 -128, Int8 能容纳的最小值
signedOverflow = signedOverflow &- 1
// signedOverflow 现在等于 127
Int8
能容纳的最小值为 -128
,或者二进制数 10000000
。用溢出运算符将其减 1
,得到二进制数 01111111
,切换了符号位并得到一个 Int8
能容纳的最大正数 127
。
对于有符号与无符号整型来说,正方向溢出就是从最大有效值环绕到最小值,而负方向溢出则是从最小有效值环绕到最大值。
优先级与关联性
运算符的 优先级 使得某些运算符比其他的拥有更高的优先级;这些运算符要优先参与运算。
运算符的 关联性 定义了拥有相同优先级的运算符是怎样组合在一起的 --- 从左侧分组或者从右侧分组。将这想象成 「它们与左边的表达式相关联」,或者 「它们与右边的表达式相关联」。
在计算复合表达式的顺序时,考虑每个运算符的关联性和优先级是很重要的。比如,运算符的关联性解释了为什么下面的表达式等于 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 的更简单,更可预测。但是,这意味着它们与基于 C 的语言不完全相同。在将现有代码移植到 Swift 时,请务必确保运算符的交互行为仍然符合你的预期。
运算符方法
类和结构体可以提供现有运算符的自有实现。也可以称为 重载 运算符。
下面的示例展示了如何为自定义结构体实现算术加法运算符( +
)。算术加法运算符是 二元运算符,因为它在两个目标上运行。并且被称为 中缀 运算符,因为它出现在两个目标之间。
该示例为二维向量 (x, y)
定义了一个结构体 Vector2D
。接下来定义了一个 运算符方法,用于将 Vector2D
的实例相加。
struct Vector2D {
var x = 0.0, y = 0.0
}
extension Vector2D {
static func + (left: Vector2D, right: Vector2D) -> Vector2D {
return Vector2D(x: left.x + right.x, y: left.y + right.y)
}
}
运算符方法被定义为 Vector2D
的类型方法,其方法名称与要重载的运算符( +
)匹配。由于加法不是向量基本行为的一部分,所以该类型方法在 Vector2D
的扩展中定义而不是在主结构中声明。另外,因为算术加法运算符是二元运算符,所以此运算符方法接受两个 Vector2D
类型的输入参数,并返回单个同样是 Vector2D
类型的输出值。
在此实现中,输入参数以 left
和 right
命名,以表示在 +
运算符左侧和右侧的 Vector2D
实例。该方法返回一个新的 Vector2D
实例,其 x
和 y
属性是由两个参数实例中的 x
和 y
相加得来。
该类型方法可以用作两个 Vector2D
实例的中缀运算符:
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)
这个例子将向量 (3.0, 1.0)
和 (2.0, 4.0)
相加得到向量 (5.0, 5.0)
,如下图所示。
前缀和后缀运算符
上面的示例展示了自定义二元中缀运算符的实现。类和结构体还可以提供标准 一元运算符 的实现。一元运算符作用于单个目标。如果位于目标之前(例如 -a
)则为 前缀 ,如果跟在目标的后面(例如 b!
)则为 后缀运算符。
声明运算符方法时,通过在 func
关键字之前写入 prefix
或 postfix
修饰符来实现前缀或后缀一元运算符的定义:
extension Vector2D {
static prefix func - (vector: Vector2D) -> Vector2D {
return Vector2D(x: -vector.x, y: -vector.y)
}
}
上面的示例给 Vector2D
实例实现了一元减运算符( -a
)。一元减运算符是前缀运算符,因此必须使用 prefix
修饰符限定此方法。
对于简单的数值,一元减运算符将正数转换为负数,反之亦然。Vector2D
的相应实现是同时对 x
和 y
执行此操作。
let positive = Vector2D(x: 3.0, y: 4.0)
let negative = -positive
// negative 是一个 Vector2D 实例,其值为 (-3.0, -4.0)
let alsoPositive = -negative
// alsoPositive 是一个 Vector2D 实例,其值为 (3.0, 4.0)
复合赋值运算符
复合赋值运算符 将赋值( =
)与另一个运算相结合。例如,加法赋值运算符( +=
)将加法和赋值组合到单个运算中。你须要将复合赋值运算符的左输入参数类型标记为 inout
,因为参数的值将直接从运算符方法中修改。
下面的例子为 Vector2D
实例实现了加法赋值运算符方法:
extension Vector2D {
static func += (left: inout Vector2D, right: Vector2D) {
left = left + right
}
}
因为加法运算符已经在之前定义过了,这里就不需要在重新实现这个过程。相反,加法赋值运算符方法利用现有的加法运算符方法,并使用它将左值赋值为左值加右值:
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
)也不能重载。
等价运算符
默认情况下,自定义类和结构体不接收等价运算符的默认实现,或者称为 等于 运算符( ==
)和 不等于 运算符( !=
)。
要使用等价运算符检查自己的自定义类型的等效性,请以与其它中缀运算符相同的方式提供「等于」运算符的实现,并添加标准库的 Equatable
协议的一致性。
extension Vector2D: Equatable {
static func == (left: Vector2D, right: Vector2D) -> Bool {
return (left.x == right.x) && (left.y == right.y)
}
}
上面的示例实现了一个「等于」运算符( ==
)来检查两个 Vector2D
实例是否具有等效值。对于 Vector2D
来说,「相等」应该意味着 「两个实例具有相同的 x
和 y
值」,这也是该运算符实现所使用的逻辑。如果你已经实现了「等于」运算符,那么通常情况下不需要再实现「不等于」运算符( !=
)。标准库提供了「不等于」运算符的默认实现,它只是否定了你实现的「等于」运算符的结果。
现在你可以用这些操作符来检查两个 Vector2D
实例是否相等:
let twoThree = Vector2D(x: 2.0, y: 3.0)
let anotherTwoThree = Vector2D(x: 2.0, y: 3.0)
if twoThree == anotherTwoThree {
print("These two vectors are equivalent.")
}
// 打印 "These two vectors are equivalent."
在很多简单的情况下,你可以让 Swift 提供等价运算符的综合实现:
- 结构体只包含符合
Equatable
协议的存储属性 - 枚举类型只包含符合
Equatable
协议的关联类型 - 枚举类型不包含关联类型
在包含这些实现的原始声明的文件中声明 Equatable
一致性。
下面的示例定义了一个 Vector3D
结构体,其具有三维坐标向量 (x, y, z)
,与 Vector2D
结构体类似。由于 x
,y
和 z
属性均为 Equatable
类型,Vector3D
能够接收等价运算符的默认实现。
struct Vector3D: Equatable {
var x = 0.0, y = 0.0, z = 0.0
}
let twoThreeFour = Vector3D(x: 2.0, y: 3.0, z: 4.0)
let anotherTwoThreeFour = Vector3D(x: 2.0, y: 3.0, z: 4.0)
if twoThreeFour == anotherTwoThreeFour {
print("These two vectors are also equivalent.")
}
// 打印 "These two vectors are also equivalent."
自定义运算符
除了 Swift 提供的标准运算符之外,你也可以声明并实现自己的 自定义运算符。有关可用于自定义运算符的字符列表可以参考 运算符。
使用 operator
关键字在全局级别声明新运算符,并使用 prefix
,infix
和 postfix
修饰符标记:
prefix operator +++
上面的例子定义了一个名为 +++
的新前缀运算符。此运算符当前在 Swift 中没有含义,因此下面的例子中, Vector2D
实例在其特定的上下文中给出了它自己的含义。在这个例子中,+++
被视为一个新的「前缀加倍」运算符。它通过使用前面定义的加法赋值运算符将向量添加到自身,使得实例的 x
和 y
值加倍。要实现 +++
运算符,可以将一个名为 +++
的类型方法添加到 Vector2D
,如下所示:
extension Vector2D {
static prefix func +++ (vector: inout Vector2D) -> Vector2D {
vector += vector
return vector
}
}
var toBeDoubled = Vector2D(x: 1.0, y: 4.0)
let afterDoubling = +++toBeDoubled
// toBeDoubled 的值为 (2.0, 8.0)
// afterDoubling 的值也为 (2.0, 8.0)
自定义中缀运算符的优先级
自定义中缀运算符均属于优先级组。优先级组指定运算符相对于其它中缀运算符的优先级,以及运算符的关联性。有关这些特征如何影响中缀运算符之间交互的说明,请参阅 优先级与关联性 。
未明确放入优先级组的自定义中缀运算符将被放入默认优先级组,其优先级高于三元条件运算符。
以下示例定义了一个名为 +-
的新自定义中缀运算符,它属于优先级组 AdditionPrecedence
:
infix operator +-: AdditionPrecedence
extension Vector2D {
static func +- (left: Vector2D, right: Vector2D) -> Vector2D {
return Vector2D(x: left.x + right.x, y: left.y - right.y)
}
}
let firstVector = Vector2D(x: 1.0, y: 2.0)
let secondVector = Vector2D(x: 3.0, y: 4.0)
let plusMinusVector = firstVector +- secondVector
// plusMinusVector 是一个 Vector2D 实例,其值为 (4.0, -2.0)
该运算符将两个向量的 x
值相加,并从第一个向量中减去第二个向量的 y
值。因为它本质上是一个「加法」运算符,所以它被添加到了与加法中缀运算符(例如 +
和 -
)相同的优先级组。有关 Swift 标准库提供的运算符的信息,包括运算符优先级组和关联性设置的完整列表,请参阅 运算符声明。有关优先级组的更多信息以及查看定义自己的运算符和优先级组的语法,请参阅 运算符声明。
注意
你不需要给前缀和后缀运算符指定优先级。但是,如果将前缀和后缀运算符同时应用于同一操作数,则首先应用后缀运算符。
本译文仅用于学习和交流目的,转载请务必注明文章译者、出处、和本文链接
我们的翻译工作遵照 CC 协议,如果我们的工作有侵犯到您的权益,请及时联系我们。
推荐文章: