高级操作符
在前面的 基本操作符(Basic Operators)之外,为了做更复杂的值操作,Swift还提供了若干高级操作符。这些高级操作符包括在C和OC中已经习以为常的按位(bitwise)和移位( bit shifting)运算符。
不同于C中的算术操作符,Swfit中的算术操作符不会默认溢出。溢出行为会被捕捉并报告为一个错误。想要选择溢出行为,需要使用Swfit中的第二算术操作符集合,比如溢出加运算符(&+).所有的溢出操作符都以&开头。
当定义自己的结构体、类和枚举时,为这些类型提供自己对于标准Swift操作符的实现是很有用的。Swift使得为这些自定义的类型量身打造标准操作符的实现变得很轻松。
预定义操作符没有任何限制,Swift提供了定制中缀、前缀、后缀和指派(定制优先级和结合性)操作符的自由。这些操作符可以在像其他预定义操作符一样使用,甚至可以通过扩展存在的类型来支持自定义的操作符。
按位操作符
按位操作符(bitwise operator)能够操作数据结构中单独bit。这在低级程序语言中经常使用,比如图形图像编程和设备驱动编程。按位操作符在处理外部数据源的数据,比如通过特定协议进行通讯时的编码和解码。
Swfit提供所有的C支持的按位操作符,下文有具体描述。
按位非操作符@H_301_18@
按位非操作符(~)将一个数字的所有bit反转:
按位非操作符是一个前置操作符,使用时要放置在要操作的数值之前,不能有空格:
let initialBits: UInt8 = 0b00001111 let invertedBits = ~initialBits // equals 11110000
UInt8整型有8bit,能够存储0到255的整型。这个例子用二进制值00001111构造了一个UInt8整型,这个二进制前4个bit都是0,接下来的4个bit都是1.它的值用十进制表示是15。
按位非操作符被用来创建一个新的常量invertedBits,这个新常量是initialBits所有bit的反转。零会被转换成一,一会转换成零。invertedBits的值是11110000,等于无符号的十进制数字240.
按位与操作符@H_301_18@
按位与操作符(&)将两个数字的bit组合在一块,只有两个操作数对应的bit都是1的时候,结果数字的对应bit才被设置为1:
在下面的例子中,firstSixBits和lastSixBits中间位置的四个bit都是1.按位与操作符将二者组合起来生成操作结果00111100,它代表无符号的十进制数字60:
let firstSixBits: UInt8 = 0b11111100 let lastSixBits: UInt8 = 0b00111111 let middleFourBits = firstSixBits & lastSixBits // equals 00111100
按位或操作符@H_301_18@
按位或操作符(|)比较两个数字的bit。这个操作符返回一个新的数字,只要操作的两个数字对应的bit有一个是1,结果数字对应的bit就被设置为1:
下面的例子,someBits和moreBits在不同的bit上是1.按位或操作符将其组合成数字11111110,表示无符号十进制254:
let someBits: UInt8 = 0b10110010 let moreBits: UInt8 = 0b01011110 let combinedbits = someBits | moreBits // equals 11111110
按位异或操作符(Bitwise XOR Operator)@H_301_18@
按位异或操作符,或者“特殊或操作符”(^),比较两个数字的bit。操作符返回一个结果数字,当操作数的对应bit数字不同时,结果数字的对应bit被设置为1;当操作数的对应bit数字相同时,结果数字对应的bit被设置为0:
下面的例子中,firstBits和otherBits各自有一个位置的bit是1但对方相同位置的不是。在按位异或运算符的结果数字中,这些位置的bit被设置为1。firstBits和otherBits中完全匹配的bit,结果数字中对应的位置bit会被设置为0:
let firstBits: UInt8 = 0b00010100 let otherBits: UInt8 = 0b00000101 let outputBits = firstBits ^ otherBits // equals 00010001
左右移位操作符@H_301_18@
左移位操作符(bitwies left shift operator)(<<)和右移位操作符(bitwise right shift operator)(>>)将操作数字的所有位向左或向右移动特定位,遵循下面定义的规则。
左移位和右移位会以2位倍数对操作数乘或者除。左移移位就是对操作数做加倍,右移一位就是对操作数减半。
无符号整型的移位@H_301_18@
无符号整型的位移需要遵循:
1:已有的bit根据参数向左或向右移动。
2:移动超出整型范围的bit被舍弃。
3:在原来的bit移动后,原来的位置用0填充。
这被称作逻辑移位。
下面的图标展示了 11111111 << 1(11111111 左移一位)的结果,和11111111 >> 1(11111111 右移一位)的结果。蓝色的数字是被移动的,灰色的数字是被舍弃的,橙色的数字是被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
这个例子使用一个UInt32常量pink来存储一个CSS(译者:Cascading Style Sheets, 级联样式表)中的颜色:粉色。CSS颜色值#CC6699依据Swift的十六进制表示法被写作0xCC6699。这个颜色被分解为它的红色(CC)、绿色(66)和蓝色(99),通过按位与操作符(&)和右移位(>>)。
红色部分通过0xCC6699 和0xFF0000按位与操作得到。0xFF0000 中的零实际上标记了0xCC6699的前两个字节,导致6699被忽略,结果中只剩下0xCC0000 。
接下来数字像右移位16位(>>16)。每个十六进制字符占用8bit,所以向右移位16位将会将0xCC0000转化为0x0000CC。这个结果和0xCC一样,就是十进制的204。
类似的,绿色部分通过0xCC6699 和0x00FF00按位与操作得到。得到的结果是0x006600。向右移8位,得到0x66,也就是十进制的102.
最后,蓝色部分通过对0xCC6699 和0x0000FF按位与操作得到。得到的结果是0x000099。这里不需要做移位了,0x000099 已经等于0x99,也就是十进制的153。
有符号整型的移位@H_301_18@
有符号整型的移位操作比无符号整型的移位要复杂的多,因为需要用二进制标记符号。(为了方便,下面的例子基于8bit的有符号整型,但是相同的规则也适用于其他尺寸的整型。)
有符号整型用它们的第一个bit(被称作符号位)来表示正负。0表示正,1表示负。
剩余的bit(称作数值位)存储实际的值。整数和无符号整型存储方式一样,从0开始计数。这里展示了一个Int8的bit如何表现数字4:
符号位是0(表示正数),7个值位表现数字4,用二进制表示。
负数的存储就不同了。实际存储的是2的n次幂减去负数的绝对值,这里n是有符号数的数字位的个数,一个8bit的数字有7个数字位,所以2的7次幂是128.
下面展示了一个Int8如何存储-4:
这次,符号位是1(表示是负数),7位2进制数字等于124(128-4):
负数的编码被称作二进制补码。看起来这是表示负数的常用方式,但它有若干优势。
首先,把-1添加到-4上,对所有8位(包括符号位)按照标准的二进制加法进行计算,舍弃这8位之外的其他位:
其次,这种表示方法能让负数向左移位,让整数向右移位,同时还能保持向左移位加倍,向右移位减半的结果。为了做到这些,有符号整形在向右移位的时候需要遵守一条额外的规则:
当将一个有符号整数向右移动时,和移动无符号数规则一样,除了需要用符号位的内容填充空位而不是用零,这一点之外。
这样确保了向右移位时符号位不变,这被称作算术移位。
通过这个特殊的存储方式,无论正数还是负数,在向右移位的时候都是像零靠拢。在移位过程中保持符号位不变,意味着负数仍然是负数,但它的值已经像零接近了。
溢出操作符(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
当数字过大或者过小时提供这样的错误处理的机会,可以增加编程的灵活度。
但是,如果是想通过溢出截断数字的有效位,就没必要出发这种错误了。好在Swift提供了5个算数溢出操作符,允许整型计算过程中的溢出行为。这些运算符都以&开头:
溢出加(&+)
溢出减(&-)
溢出乘(&*)
溢出除(&/)
溢出取余(&%)
值溢出@H_301_18@
这里有一个例子展示了当一个有符号数被允许溢出后发生的情形,当然这里用到了溢出加的操作符号(&+):
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 被用UInt8的最大值(255,或者二进制的11111111 )初始化。然后它被使用溢出加操作符(&+)添加了1。这样做迫使它的二进制表示超出了一个UInt8的范围,导致了溢出,就像下面的图一样。操作值后仍然留在UInt8表示范围内的是00000000,也就是0:
值下溢@H_301_18@
数字小于能够表现的最大范围是另外一种情况,这里有另外一个例子。
UInt8最小能表示的值是0(二进制表示00000000)。如果从00000000中减去1,这里使用溢出减操作符,数字将会变成11111111,也就是十进制的255:
这里是Swift代码:
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
类似的向下溢出也发生在有符号整型的情形。所有的有符号整型的减法被直接按照二进制计算,符号位也成了被减数的一部分,这在 向左和向右移位操作符(Bitwise Left and Right Shift Operator)中有讲过。Int8的能表现的最小值是-128,也就是二进制的10000000 。从这个最小数减去1,同样采用溢出操作符,得到二进制的01111111,符号位也反转了,得到的是正127,也就是Int8能够表现的最大数:
下面是Swift代码:
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
总结一下对于有符号和无符号整数的向上和向下溢出行为,向上溢出总是从最大的合法值到最小的合法值,向下溢出总是从最小的合法值到最大的。
0做除数
用0做除数(i / 0),或是对0取余数 (i % 0),会导致错误:
let x = 1 let y = x / 0
但是使用了溢出版本(&/和&%)会得到一个0:
let x = 1 let y = x &/ 0 // y is equal to 0
优先级别和结合性
操作符优先级给某些操作符提供了相比其他操作符的优先权,这些操作符会被先执行。
操作符结合性定义了两个优先级相等的操作符怎么组合(或结合)在一起——是从左组合,还是从右组合。意思是这些操作符是“和它左侧的表达式结合”还是“和它右侧的表达式结合”。
当面对一个复杂表达式,需要确认操作符的执行顺序时,考虑每个操作符的优先级和结合性是非常重要的。这里就有一个例子。为什么下面的表达式结果等于4?
2 + 3 * 4 % 5 // this equals 4
严格依据从左到右的顺序,会得到如下内容:
2加上3得到5;
5乘以4得到20;
20对5取余得到0
但是,实际的结果是4,而不是0。高优先级的操作符会比低优先级的操作符先执行。Swfit和C语言一样,乘法(*)和取余操作符(%)的优先级都比加法操作符(+)高。结果就是它们会在加法执行前被执行。
然而,乘法和取余运算符具有相同的优先级。为了得到准确的执行顺序,就必须考虑他们的结合性了。乘法和取余运算符都是向左结合的。设想一下从左侧给这些表达式添加上原本隐藏的括号:
2 + ((3 * 4) % 5)
(3 * 4)得到 12,所以上面等价于:
2 + (12 % 5)
(12 % 5) 得到 2,所以等价于:
2 + 2
计算的最终结果是4。
完整的Swift操作符的优先级和结合性,参见 表达式(Expressions)。
NOTE
同C语言和OC中能找到的操作符相比,Swfit的操作符的优先级和结合性很类似,但是更具可预测性。这也意味着和以C语言为基础的语言有不同之处。在使用多个操作符时要确保执行的顺序是你想要的。
操作符函数
类和结构体可以提供他们自己的对于已有操作符的实现。这被称作操作符重载。
下面的例子展示了在一个结构体内如何实现算术操作符加(+),定义了一个操作符函数来将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 类型的结果。
在这个实现中,两个参数分别没命名为left和right代表在+操作符左边和右边的Vector2D 实例。这个函数返回一个新的Vector2D 实例,它的x和y属性被初始化为被“加”到一起的两个Vector2D 实例的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 is a Vector2D instance with values of (5.0,5.0)
这个例子将向量(3.0,1.0)和(2.0,4.0)加在了一起,构造了一个新的向量(5.0,5.0),如图所示:
前缀和后缀操作符@H_301_18@
上面的例子展示了一个自定义二元中缀操作符的实现。类和结构体还可以提供标准的一元操作符的实现。一元操作符操作单一的一个对象。如果它们出现在操作对象前,它们就是前缀操作符(比如-a),如果出现在值后,就是后缀操作符(比如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 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)
组合赋值操作符(Compound Assignment Operators)@H_301_18@
组合复制操作符将赋值操作符(=)和其他操作符结合起来。比如,加赋值操作符(+=)将加操作符和赋值操作符组合在一起作为单独操作符。组合赋值操作符左侧操作数是操作符函数中用inout标记的参数,因为这个参数的值会在操作符函数内部被直接修改。
下面的例子实现了对于Vector2D 实例的加赋值操作符函数:
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)
可以用prefix或postfix修饰符修饰组合赋值操作符函数,比如为了Vector2D 实例实现前缀自增操作符(++a):
prefix func ++ (inout vector: Vector2D) -> Vector2D { vector += Vector2D(x: 1.0,y: 1.0) return vector }
上面前缀自增操作符函数使用了前面定义的高级加赋值操作符。它给调用的Vector2D 实例中的x和y的值都加了1,然后返回结果:
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)
NOTE
重载默认的赋值操作符是不可能的。只有组合赋值操作符可以被重载。类似的,三元操作符( ? b : c)也是不可以重载的。
相等操作符(Equivalence Operators)@H_301_18@
自定义的类和结构体没有得到默认的相等操作符(等于操作符==和不等于操作符!=)的实现。Swfit不可能能够猜测出对于自定义类型怎么样才是“相等”,因为“相等”需要根据这些类型所发挥的作用来定。
为了对自定义的类型也能使用相等操作符,需要像其他中缀操作符一样提供一个操作符函数实现:
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 { println("These two vectors are equivalent.") } // prints "These two vectors are equivalent."
定制操作符
可以在Swfit提供的标准操作符之外自己定义操作符。所有可以被用来自定义操作符的字符列表,参见 操作符(Operators)。
用operator关键字定义的新操作符是全局的,可以用perfix,infix或者postfix修饰符修饰:
prefix operator +++ {}
上面定义了一个新的前缀操作符+++。这个操作符Swift中没有,下面给出了针对Vector2D实例的实现。这个例子中,+++是一个新的“前缀两倍自增”操作符。它会将一个Vector2D实例的x和y变成原来的两倍,通过前面定义的加赋值操作符将结果赋值给自身:
prefix func +++ (inout vector: Vector2D) -> Vector2D { vector += vector return vector }
这个+++实现和Vector2D的++实现很类似,不同点是这个是将向量自身相加,而++只会在原来向量上加上Vector2D(1.0,1.0):
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)
定制中缀操作符的优先级和结合性@H_301_18@
自定义的中缀操作符可以指定优先级和结合性。在 优先级和结合性 (Precedence and Associativity)一节中解释了在有其他中缀操作符的时候这两个特性带来的影响。
associativity 的可能值是left、right和none。左结合操作符的后面碰到其他同等优先级的左结合操作符时,会像左结合。类似的,右结合操作符在后面碰到其他优先级相同的右结合操作符时,会向右结合。非结合操作符不能写在其他优先级别相同的操作符之后。
没有指定时,associativity 的默认值是none,precedence 的默认值是100。
下面的例子定义了一个新的自定义中缀操作符+-,它是左结合的优先级是140:
infix operator +- { associativity left precedence 140 } func +- (left: Vector2D,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值中减去第二个向量的y值。因为本质上它还算是“加”,所以它被赋予了和默认中缀操作符+和-相同的结合性和优先级。完整的Swfit中操作符的优先级设置,参见 表达式(Expressions)。
NOTE
当定义一个前缀或者后缀操作符,不必指定优先级。但是,如果同时适用一个前缀和后缀的操作符,后缀操作符会被先执行。