高级运算符
文档地址
作为 基本运算符 的补充,Swift 提供了几个高级运算符执行对数传值进行更加复杂的操作。这样写运算包括所有你从 C 或 Objective-C 所熟悉的按位操作和移位运算符。
与 C 的算术运算符不同,Swift 中算术运算符默认是不会溢出的。溢出行为都会作为错误被捕获。为了允许溢出行为,可以使用 Swift 中另一套默认支持的溢出运算符,比如溢出加法运算符(&+
)。所有这些溢出运算符都是以 &
符号开始的。
当你定义了你自己的结构体,类以及枚举的时候,那么为这些自定义类型也提供 Swift 标准的运算符将会有用的。Swift 简化了这些运算符的定制实现,并且精确地确定了你创建的每个类型的运算符所具有的行为。
你不会被预定义的运算符所限制。在 Swift 中你可以自由地定义你自己的中缀,前缀,后缀和赋值运算符,以及相对应的优先级和结合性。这些运算符可以像预先定义的运算符一样在你的代码里使用,甚至你可以扩展已存在的类型来支持你自己定义的运算符。
按位运算符
按位运算符可以操作数据结构中每一个独立的位。它们通常被用在底层开发中,比如图形编程和创建设备驱动。按位运算符在处理外部资源的原始数据时也非常有用,比如为自定义的通信协议的数据进行编码和解码。
Swift 支持 C 里面所有的按位运算符,具体如下:
按位取反运算符
按位取反运算符(~
)是对所有位的数字进行取反操作:
按位取反运算符是一个前缀运算符,需要直接放在运算符的前面,并且不能有空格:
let initialBits: UInt8 = 0b00001111
let invertedBits = ~initialBits // equals 11110000
UInt8
类型的整数有八位,可以存储 0
到 255
之间的任意值。这个例子初始化了一个 UInt8
类型的整数,二进制为 00001111
,前四位全是 0
,后四位都是 1
。这和十进制的 15
是相等的。
然后使用取反运算符创建一个新的常量名为 invertedBits
,它和 initialBits
相等,但是所有位都被取反了。0
变为了 1
,1
变为了 0
。invertedBits
的值是 11110000
,和十进制的无符号整数 240
相等。
按位与运算符
按位与运算符(&
)可以对两个数的比特位进行合并。它会返回一个新的数,只有当这两个数都是 1
的时候才能返回 1
。
在下面的例子中,firstSixBits
和 lastSixBits
的中间四个位都为 1
。按位与可以把它们合并为一个新值 00111100
,对应十进制的值为 60
。
let firstSixBits: UInt8 = 0b11111100
let lastSixBits: UInt8 = 0b00111111
let middleFourBits = firstSixBits & lastSixBits // equals 00111100
按位或运算符
按位或运算符(|
)可以对两个比特位进行比较,然后返回一个新数,只要两个操作位任意一个为 1
时,那么对应的位数都为 1
:
在下面的例子中,someBits
和 moreBits
在不同的位设置了 1
。按位或运算符把它们合并为 11111110
,对应的十进制是无符号整数 154
。
let someBits: UInt8 = 0b10110010
let moreBits: UInt8 = 0b01011110
let combinedbits = someBits | moreBits // equals 11111110
按位异或运算符
按位异或运算符(^
)可以对两个数的比特位进行比较。它返回一个新的数,当两个操作数的对应位不相同时,该数的对应位就为 1
:
在下面的例子中,firstBits
和 otherBits
的值有一位设置设置为 1
,而对方设置为 0
。按位异或运算符会将这两个位上的值设置为 1
,firstBits
和 otherBits
其他位都设置为了 0
。
let firstBits: UInt8 = 0b00010100
let otherBits: UInt8 = 0b00000101
let outputBits = firstBits ^ otherBits // equals 00010001
按位左移或右移运算符
按位左移(<<
)或右移(>>
)运算符可以把所有位数的数字向左或向右移动一个确定的位数,但是需要遵守下面定义的规则。
按位左移或右移具有乘以 2
或除以 2
的效果。将一个数左移一位相当于把这个数乘以 2
,将一个数右移一位相当于把这个数除以 2
。
无符号整数的移位操作
对无符号整数的移位规则如下:
-
已经存在的比特位按指定的位数进行左移和右移。
-
任何移动超出整型存储边界的位都会被丢弃。
-
用
0
来填充向左或向右移动后产生的空白位。
这种方法称之为 逻辑操作。
下图展示了 11111111 << 1
(即把 11111111
向左移动 1
位),11111111 >> 1
(即把 11111111
向右移动 1
位),蓝色的数字是被移位的,灰色的数字被舍弃,橙色的数字 0
是新插入的:
下面的代码展示了 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
这个示例使用了一个命名为 pink
的 UInt32
型常量来存储层叠样式表(CSS
)中粉色的颜色值。该 CSS
的十六进制颜色值 #CC6699
, 在 Swift 中表示为 0xCC6699
。然后利用按位与运算符(&
)和按位右移运算符(>>
)从这个颜色值中分解出红(CC
)、绿(66
)以及蓝(99
)三个部分。
红色部分是通过对 0xCC6699
和 0xFF0000
进行按位与运算后得到的。0xFF0000
中的 0
部分作为掩码,掩盖了 OxCC6699
中的第二和第三个字节,使得数值中的 6699
被忽略,只留下 0xCC0000
。
然后,再将这个数按向右移动 16
位(>> 16
)。十六进制中每两个字符表示 8
个比特位,所以移动 16
位后 0xCC0000
就变为 0x0000CC
。这个数和 0xCC
是等同的,也就是十进制数值的 204
。
同样的,绿色部分通过对 0xCC6699
和 0x00FF00
进行按位与运算得到 0x006600
。然后将这个数向右移动 8
位,得到 0x66
,也就是十进制数值的 102
。
最后,蓝色部分通过对 0xCC6699
和 0x0000FF
进行按位与运算得到 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
) 的二进制表示。
负数的表示通常被称为二进制补码(two's complement)表示法。用这种方法来表示负数乍看起来有点奇怪,但它有几个优点。
首先,如果想对 1
和 -4
进行加法操作,我们只需要将这两个数的全部 8
个比特位进行相加,并且将计算结果中超出 8
位的数值丢弃:
其次,使用二进制补码可以使负数的按位左移和右移操作得到跟正数同样的效果,即每向左移一位就将自身的数值乘以 2
,每向右一位就将自身的数值除以 2
。要达到此目的,对有符号整数的右移有一个额外的规则:
- 当对正整数进行按位右移操作时,遵循与无符号整数相同的规则,但是对于移位产生的空白位使用符号位进行填充,而不是用
0
。
这个行为可以确保有符号整数的符号位不会因为右移操作而改变,这通常被称为算术移位(arithmetic shift)。
由于正数和负数的特殊存储方式,在对它们进行右移的时候,会使它们越来越接近 0
。在移位的过程中保持符号位不变,意味着负整数在接近 0
的过程中会一直保持为负。
溢出运算符
在默认情况下,当向一个整数赋超过它容量的值时,Swift 默认会报错,而不是生成一个无效的数。这个行为给我们操作过大或着过小的数的时候提供了额外的安全性。
例如,Int16
型整数能容纳的有符号整数范围是 -32768
到 32767
,当为一个 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
。
同样地,当我们对一个无符号整数使用溢出减法(&-
)进行下溢运算时也会产生类似的现象:
var unsignedOverflow = UInt8.min
// unsignedOverflow 等于 UInt8 所能容纳的最小整数 0
unsignedOverflow = unsignedOverflow &- 1
// 此时 unsignedOverflow 等于 255
UInt8
型整数能容纳的最小值是 0
,以二进制表示即 00000000
。当使用溢出减法运算符(&-
)对其进行减 1
操作时,数值会产生下溢并被截断为 11111111
, 也就是十进制数值的 255
。
溢出也会发生在有符号整型数值上。在对有符号整型数值进行溢出加法或溢出减法运算时,符号位也需要参与计算,正如按位左移/右移运算符所描述的。
var signedOverflow = Int8.min
// signedOverflow 等于 Int8 所能容纳的最小整数 -128
signedOverflow = signedOverflow &- 1
// 此时 signedOverflow 等于 127
Int8
型整数能容纳的最小值是 -128
,以二进制表示即 10000000
。当使用溢出减法操作符对其进行减 1
操作时,符号位被翻转,得到二进制数值 01111111
,也就是十进制数值的 127
,这个值也是 Int8
型整数所能容纳的最大值。
对于无符号与有符号整型数值来说,当出现上溢时,它们会从数值所能容纳的最大数变成最小的数。同样地,当发生下溢时,它们会从所能容纳的最小数变成最大的数。
优先级和结合性
运算符的优先级(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的语言不是完全一致的。在对现有的代码进行移植的时候,要注意确保运算符的行为仍然是按照你所想的那样去执行。
运算符函数
类和结构体可以为现有的操作符提供自定义的实现,这通常被称为运算符重载(overloading)。
下面的例子展示了如何为自定义的结构实现加法操作符(+
)。算术加法运算符是一个两目运算符(binary operator),因为它可以对两个目标进行操作,同时它还是中缀(infix)运算符,因为它出现在两个目标中间。
例子中定义了一个名为 Vector2D 的结构体用来表示二维坐标向量 (x, y)
,紧接着定义了一个可以对两个 Vector2D 结构体进行相加的运算符函数(operator function):
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 类型的返回值。
在这个实现中,输入参数分别被命名为 left
和 right
,代表在 +
运算符左边和右边的两个 Vector2D 对象。函数返回了一个新的 Vector2D 的对象,这个对象的 x
和 y
分别等于两个参数对象的 x
和 y
的值之和。
这个函数被定义成全局的,而不是 Vector2D 结构的成员方法,所以任意两个 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)
。这个过程如下图示:
前缀和后缀运算符
上个例子演示了一个双目中缀运算符的自定义实现。类与结构体也能提供标准单目运算符(unary operators)的实现。单目运算符只有一个操作目标。当运算符出现在操作目标之前时,它就是前缀(prefix)的(比如 -a
),而当它出现在操作目标之后时,它就是后缀(postfix)的(比如 i++
)。
要实现前缀或者后缀运算符,需要在声明运算符函数的时候在 func
关键字之前指定 prefix 或者 postfix 限定符:
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 是一个值为 (-3.0, -4.0) 的 Vector2D 实例
let alsoPositive = -negative
// alsoPositive 是一个值为 (3.0, 4.0) 的 Vector2D 实例
复合赋值运算符
复合赋值运算符(Compound assignment operators)将赋值运算符(=
)与其它运算符进行结合。比如,将加法与赋值结合成加法赋值运算符(+=
)。在实现的时候,需要把运算符的左参数设置成 inout 类型,因为这个参数的值会在运算符函数内直接被修改。
func += (inout left: 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
进行重载.
等价操作符
Custom classes and structures do not receive a default implementation of the equivalence operators, known as the “equal to” operator (==) and “not equal to” operator (!=). It is not possible for Swift to guess what would qualify as “equal” for your own custom types, because the meaning of “equal” depends on the roles that those types play in your code.
To use the equivalence operators to check for equivalence of your own custom type, provide an implementation of the operators in the same way as for other infix operators:
自定义的类和结构体没有对等价操作符(equivalence operators)进行默认实现,等价操作符通常被称为『相等』操作符(==
)与『不等』操作符(!=
)。对于自定义类型,Swift 无法判断其是否『相等』,因为『相等』的含义取决于这些自定义类型在你的代码中所扮演的角色。
为了使用等价操作符来对自定义的类型进行判等操作,需要为其提供自定义实现,实现的方法与其它中缀运算符一样:
func == (left: Vector2D, right: Vector2D) -> Bool {
return (left.x == right.x) && (left.y == right.y)
}
func != (left: Vector2D, right: Vector2D) -> Bool {
return !(left == right)
}
上述代码实现了『相等』运算符(==
)来判断两个 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 当中还可以声明和实现自定义运算符(custom operators)。可以用来自定义运算符的字符列表请参考操作符
新的运算符要在全局作用域内,使用 operator
关键字进行声明,同时还要指定 prefix
、infix
或者 postfix
限定符:
prefix operator +++ {}
上面的代码定义了一个新的名为 +++
的前缀运算符。对于这个运算符,在 Swift 中并没有意义,因为我们针对 Vector2D
的实例来定义它的意义。对这个示例来讲,+++
被实现为『前缀双自增』运算符。它使用了前面定义的复合加法操作符来让矩阵对自身进行相加,从而让 Vector2D
实例的 x
属性和 y
属性的值翻倍:
prefix func +++ (inout vector: Vector2D) -> Vector2D {
vector += vector
return vector
}
Vector2D
的 +++
的实现和 ++
的实现很相似, 唯一不同的是前者对自身进行相加, 而后者是与另一个值为 (1.0, 1.0)
的向量相加.
var toBeDoubled = Vector2D(x: 1.0, y: 4.0)
let afterDoubling = +++toBeDoubled
// toBeDoubled 现在的值为 (2.0, 8.0)
// afterDoubling 现在的值也为 (2.0, 8.0)
自定义中缀运算符的优先级和结合性
自定义的中缀(infix)运算符也可以指定优先级(precedence)和结合性(associativity)。优先级和结合性中详细阐述了这两个特性是如何对中缀运算符的运算产生影响的。
结合性(associativity)可取的值有 left
,right
和 none
。当左结合运算符跟其他相同优先级的左结合运算符写在一起时,会跟左边的操作数进行结合。同理,当右结合运算符跟其他相同优先级的右结合运算符写在一起时,会跟右边的操作数进行结合。而非结合运算符不能跟其他相同优先级的运算符写在一起。
结合性(associativity)的默认值是 none
,优先级(precedence)如果没有指定,则默认为 100
。
以下例子定义了一个新的中缀运算符 +-
,此操作符是左结合的,并且它的优先级为 140
:
infix operator +- { associativity left precedence 140 }
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
值减去第二个向量的 y
值。因为它本质上是属于『加型』运算符,所以将它的结合性和优先级被设置(left
和 140
),这与 +
和 -
等默认的中缀加型操作符是相同的。完整的 Swift 操作符默认结合性与优先级请参考表达式。
注意:
当定义前缀与后缀操作符的时候,我们并没有指定优先级。然而,如果对同一个操作数同时使用前缀与后缀操作符,则后缀操作符会先被执行。
网友评论