二十三、高级运算符 Advanced Operators
1. 概述
除了二、基本数据类型中所讲的运算符,Swift还有许多复杂的高级运算符,包括了C语和Objective-C中的位运算符和移位运算。
不同于C语言中的算术运算符 arithmetic operators ,Swift 的算术运算符默认是不可溢出的 overflow。溢出行为会被捕获并报告为错误。当然,如果你需要溢出的话,Swift也也提供了另一套默认允许溢出的算术操作符,如可溢出加 &+
。所有允许溢出的操作符都是以 &
开始的。
当你定义自己的类、结构体、枚举的时候,可以提供它们的基本操作符的自定义实现。
在Swift中,你除了可以自定义这些已经预定义 predefined 的操作符的具体实现,你还可以自由定义中缀 infix、前缀 prefix、后缀 postfix 和赋值操作符,同时还能自定义这些操作符的优先级 precedence 和 关联值 associativity values。这些自定义的操作符可以像Swift预定义的操作符一样在代码中使用,甚至扩展一些已存在的类型,使它们支持这些自定义的操作符。
2. 位运算操作符(按位操作符) Bitwise Operators
位运算操作符允许你按位 bit 操作数据,一般使用在底层程序中,比如图形处理和驱动程序中。也可以用于处理元数据 raw data。
Swift 支持 C语言中所有的位运算符。
2.1 按位取反 Bitwise NOT Operator
按位取反运算符~ 对一个操作数的每一位都取反。
~ 是前缀 prefix 操作符,出现在操作数前面,之间没有空格 。
let initialBits: UInt8 = 0b00001111 // 使用二进制数表示 let invertedBits = ~initialBits // equals 11110000
2.2 按位与 Bitwise AND Operator
按位与运算符 & 对两个数进行操作,然后返回一个新的数,个输入数的同一位都为1时才为1。
let firstSixBits: UInt8 = 0b11111100 let lastSixBits: UInt8 = 0b00111111 let middleFourBits = firstSixBits & lastSixBits // equals 00111100
2.3 按位或 Bitwise OR Operator
按位与运算符 | 对两个数进行操作,然后返回一个新的数,两个输入数的同一位有一个为1时就为1。
let someBits: UInt8 = 0b10110010 let moreBits: UInt8 = 0b01011110 let combinedbits = someBits | moreBits // equals 11111110
2.4 按位异或 Bitwise XOR Operator
按位与运算符 ^ 对两个数进行操作,然后返回一个新的数,两个输入数的同一位不相同时就为1,相同时为0。
let firstBits: UInt8 = 0b00010100 let otherBits: UInt8 = 0b00000101 let outputBits = firstBits ^ otherBits // equals 00010001
2.5 按位左移,按位右移 Bitwise Left and Right Shift Operators
左移运算符<<
和右移运算符>>
会把一个数的所有比特位按以下定义的规则向左或向右移动指定位数。
按位左移和按位右移的效果相当把一个整数乘于或除于一个因子为2
的整数。向左移动一个整型的比特位相当于把这个数乘于2
,向右移一位就是除于2
。
2.5.1 无符号整形的位移操作 Shifting Behavior for Unsigned Integers
已经存在的比特位向左或向右移动指定的位数。被移出整型存储边界的的位数直接抛弃,移动留下的空白位用零0
来填充。这种方法称为逻辑移位。
以下这张把展示了 11111111 << 1
(11111111
向左移1位),和 11111111 >> 1
(11111111
向右移1位)。蓝色的是被移位的,灰色是被抛弃的,橙色的0
是被填充进来的。
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
这个例子使用了一个UInt32
的命名为pink
的常量来存储层叠样式表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
。、
2.5.2 有符号整形的位移操作 Shifting Behavior for Signed Integers
有符整型的移位操作相对复杂得多,因为正负号也是用二进制位表示的。(这里举的例子虽然都是8位的,但它的原理是通用的。)
有符整型通过第1个比特位(称为符号位)来表达这个整数是正数还是负数。0
代表正数,1
代表负数。
其余的比特位(称为数值位)存储其实值。有符正整数和无符正整数在计算机里的存储结果是一样的,下来我们来看+4
内部的二进制结构。
符号位为0
,代表正数,另外7比特位二进制表示的实际值就刚好是4
。
负数呢,跟正数不同。负数存储的是2的n次方减去它的绝对值(subtracting their absolute value from
2
to the power of n
),n为数值位的位数。一个8比特的数有7个数值位(即8减去一个符号位),所以是2的7次方,即128。
我们来看-4
存储的二进制结构。
现在符号位为1
,代表负数,7个数值位要表达的二进制值是124,即128 - 4。
负数的编码方式称为二进制补码(two’s complement representation)表示。这种表示方式看起来很奇怪,但它有几个优点。
首先,只需要对全部8个比特位(包括符号)做标准的二进制加法就可以完成 -1 + -4
的操作,忽略加法过程产生的超过8个比特位表达的任何信息。
第二,由于使用二进制补码表示,我们可以和正数一样对负数进行按位左移右移的,同样也是左移1位时乘于2
,右移1位时除于2
。要达到此目的,对有符整型的右移有一个特别的要求:
对有符号整型按位右移时,使用符号位(正数为0
,负数为1
)填充空白位。
这就确保了在右移的过程中,有符整型的符号不会发生变化。这称为算术移位。
正因为正数和负数特殊的存储方式,向右移位使它接近于0
。移位过程中保持符号会不变,负数在接近0
的过程中一直是负数。
3. 溢出运算 Overflow Operators
默认情况下,当你往一个整型常量或变量赋于一个它不能承载的大数时,Swift不会让你这么干的,它会报错。这样,在操作过大或过小的数的时候就很安全了。
例如,Int16
整型能承载的整数范围是-32768
到32767
,如果给它赋上超过这个范围的数,就会报错:
var potentialOverflow = Int16.max // potentialOverflow equals 32767, which is the largest value an Int16 can hold potentialOverflow += 1 // this causes an error
对过大或过小的数值进行错误处理让你的数值边界条件更灵活。
当然,你有意在溢出时对有效位进行截断 truncate the number of available bits,你可采用溢出运算,而非触发错误。Swfit为整型计算提供了5个&
符号开头的溢出运算符。
- 溢出加法
&+
- 溢出减法
&-
- 溢出乘法
&*
- 溢出除法
&/
- 溢出求余
&%
3.1 值的上溢出 Value Overflow
下面的例子展示了一个允许溢出的无符号值使用溢出加法 &+ 的情况
var willOverflow = UInt8.max // willOverflow equals 255, which is the largest value a UInt8 can hold willOverflow = willOverflow &+ 1 // willOverflow is now equal to 0
变量 willOverflow
被初始化为8为整形能存储的最大值(255,或者说二进制 11111111),使用溢出加1后,将导致越界overflow beyond its bounds,如下图所示,越界之后,在UInt8范围内的值是00000000也就是0:
3.2 值的下溢出 Value Underflow
数值也有可能因为太小而越界。举个例子:
UInt8
的最小值是0
(二进制为00000000
)。使用&-
进行溢出减1,就会得到二进制的11111111
即十进制的255
。
var willUnderflow = UInt8.min // willUnderflow equals 0, which is the smallest value a UInt8 can hold willUnderflow = willUnderflow &- 1 // willUnderflow is now equal to 255
有符整型也有类似的下溢出,有符整型所有的减法也都是对包括在符号位在内的二进制数进行二进制减法的,这在 "按位左移、按位右移运算符" 一节提到过。最小的有符整数是-128
,即二进制的10000000
。用溢出减法减去去1后,变成了01111111
,即UInt8所能承载的最大整数127
。
var signedUnderflow = Int8.min // signedUnderflow equals -128, which is the smallest value an Int8 can hold signedUnderflow = signedUnderflow &- 1 // signedUnderflow is now equal to 127
对于所有有符号整形和无符号整形的上溢出和下溢出,上述的结果都是一样的,上溢出总是从最大的有效数 largest valid integer value 变成最小的,下溢出总是从最小的有效数变成最大的。
3.3 除以0 Division by Zero
一个数除以0 (i / 0
),或是对0取余 calculate remainder by zero(i % 0
),将导致错误:
let x = 1 let y = x / 0 //error
但是overflow的版本 (&/
and &%
) 依然返回0,而不会触发错误:
let x = 1 let y = x &/ 0 // y is equal to 0
4. 运算符函数 Operator Functions
让已有的运算符也可以对自定义的类和结构进行运算,这称为运算符重载。
下面的例子展示了如何对自定义的结构体使用 + 符号,+ 是双目运算符 binary operator,因为它必须出现在两个操作数之间,也就是说说的 infix。
例子中定义了一个名为Vector2D
的二维坐标向量 (x,y)
的结构,然后定义了让两个Vector2D
的对象相加的运算符函数:
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结构体中,所以可以在Vector2D实例之间使用这个双目运算符:
let vector = Vector2D(x: 3.0, y: 1.0) let anotherVector = Vector2D(x: 2.0, y: 4.0) let combinedVector = vector + anotherVector // combinedVector is a Vector2D instance with values of (5.0, 5.0)
这个例子实现两个向量 (3.0,1.0)
和 (2.0,4.0)
相加,得到向量 (5.0,5.0)
的过程。如下图示:
4.1 前缀和后缀运算符 Prefix and Postfix Operators
上面的例子自定义了双目的中缀infix运算符的实现,类和结构体也可以提供标准的单目运算符 unary operators 的实现。单目运算符只有单一的操作数,前缀运算符 prefix 放在操作数前面,比如 -a,后缀运算符 postfix 放在操作数的后面,比如 i++。
使用 prefix 和 postfix 定义单目前缀和后缀运算符:
prefix func - (vector: Vector2D) -> Vector2D { return Vector2D(x: -vector.x, y: -vector.y) }
那么:
let positive = Vector2D(x: 3.0, y: 4.0) let negative = -positive // negative is a Vector2D instance with values of (-3.0, -4.0) let alsoPositive = -negative // alsoPositive is a Vector2D instance with values of (3.0, 4.0)
4.2 复合赋值运算符 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 now has values of (4.0, 6.0)
也可以将前面定义前缀或后缀运算符进行复合,比如++a:
prefix func ++ (inout vector: Vector2D) -> Vector2D { vector += Vector2D(x: 1.0, y: 1.0) return vector }
那么:
var toIncrement = Vector2D(x: 3.0, y: 4.0) let afterIncrement = ++toIncrement // toIncrement now has values of (4.0, 5.0) // afterIncrement also has values of (4.0, 5.0)
注意:重载等号操作符 = 是不被允许的,只有复合赋值类型能被重载。类似的,三元条件运算符 ternary conditional operator (a ? b : c
)也不能被重载。
4.3 等值运算符 Equivalence Operators
自定义的类和结构体默认情况下并没有等值操作符,也就是 equal to == 和 not equal to !=。
使用等值操作符检查自定义类型是否等值,等值操作符的实现与上面的其他 infix 操作符相同:
func == (left: Vector2D, right: Vector2D) -> Bool { return (left.x == right.x) && (left.y == right.y) } func != (left: Vector2D, right: Vector2D) -> Bool { return !(left == right) }
那么:
let twoThree = Vector2D(x: 2.0, y: 3.0) let anotherTwoThree = Vector2D(x: 2.0, y: 3.0) if twoThree == anotherTwoThree { println("These two vectors are equivalent.") } // prints "These two vectors are equivalent."
5. 自定义运算符 Custom Operators
除了Swift提供的标准运算符外,你可以定义自己的操作符。对于可以用来自定义的操作符,参见Operators. 和 二、基本数据类型
通过 operator
关键字一个声明新的全局的 global level 操作符,并且加上 prefix
, infix
or postfix
标识:
prefix operator +++ {}
上面的代码定义了一个新的操作符 +++,Swift中并不存在这个操作符,我们可以自定义这个操作符的含义。
比如,我们将 +++ 定义为 Vector2D 的前缀双倍自加 prefix doubling incrementer 运算符:
prefix func +++ (inout vector: Vector2D) -> Vector2D { vector += vector return vector }
那么:
var toBeDoubled = Vector2D(x: 1.0, y: 4.0) let afterDoubling = +++toBeDoubled // toBeDoubled now has values of (2.0, 8.0) // afterDoubling also has values of (2.0, 8.0)
5.1 自定义中缀运算符的优先级和结合性 Precedence and Associativity for Custom Infix Operators
你可以给自定义中缀运算符指定优先级和结合性。
结合性可能的值是 associativity
are left
, right
, and none
。左结合运算符 Left-associative 跟其他优先级相同的左结合运算符写在一起时,会跟左边的操作数结合。同理,右结合运算符 right-associative 会跟右边的操作数结合。而非结合运算符 Non-associative 不能跟其他相同优先级的运算符写在一起。
如果不指定结合性的话,结合性(associativity
)的默认值是 none。如果不指定优先级,优先级(precedence
)的默认值是 100.
以下例子定义了一个新的中置运算符 infix
+-
,定义它的结合性为left
,优先级为140
:
infix operator +- {
associativity left precedence 140 // 结合性为left
,优先级为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 is a Vector2D instance with values of (4.0, -2.0)
这个运算符把两个向量的x
相加,把向量的y
相减。因为他实际是属于加减运算,所以让它保持了和加法一样的结合性和优先级(left
和140
)。查阅完整的Swift默认结合性和优先级的设置,请移步Expressions.
注意:当定义 prefix 运算符 或 postfix 运算符时,不用指定优先级,但是当你对一个操作数 operand 同时指定了 前缀运算符和 后缀运算符时,postfix 会优先执行 the postfix operator is applied first.