高级运算符(Advanced Operators)
本文参考自苹果官方文档Advanced Operators
本页内容包括
- 位运算符(Bitwise Operators)
- 溢出运算符(Overflow Operators)
- 优先级和结合性(Precedence and Associativity)
- 运算符函数(Operator Functions)
- 自定义运算符(Custom Operators)
除了在基本运算符(Basic Operator)中介绍的运算符外,Swift提供一些对值进行复杂操作的高级运算符
这些高级运算符包含在C和Objective-C中大家所熟知的位运算符和移位运算符。
与C语言中的算术运算符不同的是,Swift中的算术运算符默认不溢出.
溢出行为都会被捕获并报告为错误.
如果要允许溢出行为,可以使用Swift中第二套默认支持溢出的运算符,比如溢出加法运算符(&+).
所有的这些溢出运算符都是以&开头
当你自定义结构体,类和枚举时,为它们提供对标准的Swift运算符自定义实现将会非常有用.
在Swift中为这些运算符提供定制的实现和为每一种你创建的类型决定运算符的行为都是很简单的.
你不会被预定义的运算符所限制.
**Swift给予你用自定义优先级和结合性来自定义前缀/中缀/后缀/赋值运算符的自由. **
这些运算符在代码中可以像任意预定义的运算符一样被使用,我们甚至可以扩展已有的类型以支持你自定义的运算符。
位运算符(Bitwise Operators)
位运算符使你可以操作数据结构中独立的原始的位. 它们通常被用在底层开发中,比如图形编程和创建设备驱动.位运算符在处理外部资源的原始数据时也十分有用,比如对自定义协议的通信传输的数据进行编码和解码。
Swift支持如下所述的C语言中的全部位运算符
按位取反运算符
按位取反运算符(~)可以对一个数值的全部位进行取反:
按位取反运算符是一个前缀运算符,出现在被操作数之前,之间不能添加任何空格:
<pre><code>let initialBits : UInt8 = 0b00001111 let invertedBits = ~initialBits//等于0b11110000
</code></pre>
UInt8类型的整数有8个位,可以存储0~255之间的任意值.这个例子用二进制的值00001111初始化了一个UInt8类型的整数,它的前4位都为0,后4位都为1.这个值等价于十进制的15.
之后用按位取反运算符创建了一个名为invertedBits的常量.其值与全部位取反后的initialBits相等.即所有的0都变成了1,同时所有的1都变成0.invertedBits的二进制值为11110000,等价于无符号十进制的240
按位与运算符
按位与运算符(&)可以对两个数的位进行合并.它返回一个新的数,其位会被设置为1,只有当两个数的位都为1时.
在下例中,firstSixBits和lastSixBits中间4个位都为1.按位与运算符对它们进行了运算得到数00111100,等价于无符号十进制数60:
<pre><code>let firstSixBits: UInt8 = 0b11111100 let lastSixBits: UInt8 = 0b00111111 let middleFourBits = firstSixBits & lastSixBits //等于00111100
</code></pre>
按位或运算符
按位或运算符(|)对两个数的位进行比较.它返回一个新的数,其位被设置为1,如果任意一个数的位为1.
在下例中,按位或运算符对someBits和moreBits进行了运算得到数11111110,等价于无符号十进制数254:
<pre><code>let someBits: UInt8 = 0b10110010 let moreBits: UInt8 = 0b01011110 let combinedbits = someBits | moreBits //等于11111110
</code></pre>
按位异或运算符
按位异或运算符(^)对两个数的位进行比较.它返回一个新的数,其位被设置为1当两个数的位不相同时,其位被设置为0当两个数的位相同时.
在下例中,firstBits和otherBits都有一个位得值为1而另一个对应的不是1.按位异或运算符将结果中的这两个位都设置为1,将其它位设置为0:
<pre><code>let firstBits: UInt8 = 0b00010100 let otherBits: UInt8 = 0b00000101 let outputBits = firstBits ^ otherBits // 等于 00010001
</code></pre>
按位左移/右移运算符
按位左移运算符(<<)和按位右移运算符(>>)可以对一个数的所有位进行指定位数的左移和右移,根据下面定义的规则
将一个整数左移一位相当于将这个数乘以2,同样地将一个整数右移一位相当于将这个数除以2
无符号整数的移位运算
对无符号整数进行移位的规则如下:
- 已经存在的位按指定的位数进行左移和右移
- 任何因移动而超出整型存储范围的位都会被丢弃
- 用0来填充移位后遗留的空白位
这种方法被称为逻辑移位
下图展示了11111111 << 1(即把11111111向左移动1位)和 11111111 >> 1(即把 11111111向右移动1位)的结果.
蓝色的部分是被移位的,灰色的部分是被抛弃的,橙色的部分则是被填充进来的:
这里是Swift中的移位运算:
<pre><code>let shiftBits: UInt8 = 4 // 00000100 shiftBits << 1 // 00001000 shiftBits << 2 // 00010000 shiftBits << 5 // 10000000 shiftBits << 6 // 00000000 shiftBits >> 2 // 00000001
</code></pre>
你可以使用移位运算对其他的数据类型进行编码和解码:
<pre><code>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
</code></pre>
这个示例使用了一个命名为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位的有符号整数的,但是其中的原理对任何位数的有符号整数都是通用的)
有符号整数使用第1个位(通常被称为符号位)来表示这个数的正负.符号位为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位的数值丢弃:
其次,用二进制补码可以使负数如同正数那样按位左移和右移运算,即每向左移一位就将自身的数值乘以2,每向右一位就将自身的数值除以2.要达到此目的,对有符号整数的右移有一个额外的规则:
当对整数进行按位右移运算时,遵循与无符号整数相同的规则,但是对于移位产生的空白位使用符号位进行填充,而不是用0
这个行为可以确保有符号整数在右移运算后符号位不会改变
这被称为算术移位
由于正数和负数的特殊存储方式,在对它们进行右移的时候,会使它们越来越接近0.
在移位的过程中保持符号位不变,意味着负整数在接近0的过程中会一直保持为负
溢出运算符(Overflow Operators)
如果你尝试为一个整数类型常量或变量赋予超过其承载范围的值,Swift默认会报错而不会生成一个无效的值.这个行为在你使用过大或过小的数的时候提供了额外的安全性.
例如,Int16类型整数能容纳的有符号整数范围是-32768到32767,当为一个Int16型变量赋的值超过这个范围时,系统就会报错:
<pre><code>var potentialOverflow = Int16.max //potentialOverflow的值是32767,这是Int16能容纳的最大整数 potentialOverflow += 1 //这里会报错:Arithmetic operation '32767 + 1' (on type 'Int16') results in an overflow
</code></pre>
当值变得过大或者过小时提供错误处理能让你在对边界值进行编码时更加灵活
然而,当你想要在溢出时截取有效值,你可以采用溢出运算而不是错误处理.Swift提供了3个溢出运算符来支持整数计算的溢出行为.这些运算符都是以(&)开头的:
- 溢出加法(&+)
- 溢出减法(&-)
- 溢出乘法(&)*
数值溢出
数值可以在正反方向上溢出
如下演示了当使用溢出加法(&+),允许一个无符号整数上溢时会发生什么:
<pre><code>var unsignedOverflow = UInt8.max //unsignedOverflow等于UInt8所能容纳的最大整数255 unsignedOverflow = unsignedOverflow &+ 1 //此时unsignedOverflow等于0
</code></pre>
unsignedOverflow变量被初始化为UInt8所能容纳的最大整数(255,以二进制表示即11111111).然后使用了溢出加法运算符(&+)对其进行加1运算.这使得它的二进制表示正好超出UInt8所能容纳的位数,也就导致了数值的溢出,如下图所示.数值溢出后,留在UInt8边界内的值是00000000,也就是十进制的0
当我们允许一个无符号整数下溢时也会产生类似的现象.这是一个使用溢出减法(&-)的示例:
<pre><code>var unsignedOverflow = UInt8.min // unsignedOverflow等于UInt8所能容纳的最小整数0 unsignedOverflow = unsignedOverflow &- 1 // 此时 unsignedOverflow 等于 255
</code></pre>
UInt8型整数能容纳的最小值是0,以二进制表示即00000000.当使用溢出减法运算符对其进行减1运算时,数值会产生下溢并被截断为11111111,也就是十进制的255
溢出也会发生在有符号整型数值上.在对有符号整型数值进行溢出加法或溢出减法运算时,符号位也需要参与计算,如同按位左移/右移运算符所描述的那样
<pre><code>var signedOverflow = Int8.min //signedOverflow等于Int8所能容纳的最小整数-128 signedOverflow = signedOverflow &- 1 //此时signedOverflow等于127
</code></pre>
Int8类型整数能容纳的最小值是-128,以二进制表示即10000000.当使用溢出减法运算符对其进行减1运算时,符号位被翻转,得到二进制数值01111111,也就是十进制数值的127,这个值也是Int8型整数所能容纳的最大值.
对于无符号与有符号整型数值来说,在正方向溢出时会从所能容纳的最大值变成最小值,在负方向溢出时会从所能容纳的最小值变成最大值
优先级和结合性(Precedence and Associativity)
运算符的优先级 使一些运算符的级别高于其他运算符;高优先级的运算符会先被使用.
运算符的结合性 定义了相同优先级的运算符是如何结合的,是与左边结合为一组,还是与右边结合为一组.可理解为“它们是与左边的表达式结合的”,或者“它们是与右边的表达式结合的”.
在复合表达式的运算顺序中,考虑运算符的优先级和结合性是非常重要的.例如,运算符优先级解释了为什么下面这个表达式的结果会是17
<pre><code>2 + 3 % 4 * 5 // 结果是 17
</code></pre>
如果你死板的从左到右进行阅读,表达式的运算会是这样的:
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运算符优先级和结合性规则,请参考Expressions.
Swift标准库提供所有的运算符的信息,请查看Swift Standard Library Operators Reference
注意
Swift的运算符优先级和结合性规则比C语言和Objective-C中的更加简单和可预测.但是,这意味着它们在基于C的语言中并不是完全一致的.在把现有的代码移植到Swift的时候,要注意确保运算符的行为仍然符合你的想法
运算符函数(Operator Functions)
类和结构体可以为现有的运算符提供自定义的实现,被称为运算符重载
下例中展示了如何为自定义的结构体实现加法运算符(+).算术加法运算符是一个双目运算符,因为它对两个值进行运算,同时它还是中缀运算符,因为它出现在两个值中间.
下例中定义了一个名为Vector2D的结构体用来表示二维坐标向量(x, y),紧接着定义了一个可以对两个Vector2D结构体进行相加的运算符函数:
<pre><code>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) }
</code></pre>
该运算符函数被定义为一个全局函数,并且函数的名字与它要进行重载的+名字一致.因为算术加法运算符是双目运算符,所以这个运算符函数接收两个类型为Vector2D的参数,同时有一个Vector2D类型的返回值.
在这个实现中,输入参数分别被命名为left和right,代表在+运算符左边和右边的两个Vector2D实例.函数返回了一个新的Vector2D实例,这个实例的x和y分别等于作为参数的两个实例的x和y的值之和.
这个函数被定义成全局的,而不是Vector2D结构体的成员方法,所以任意两个Vector2D实例都可以使用这个中缀运算符:
<pre><code>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) print(combinedVector.x,combinedVector.y) //5.0 5.0
</code></pre>
这个例子实现两个向量(3.0,1.0)和(2.0,4.0)的相加,并得到新的向量(5.0,5.0).这个过程如下图示:
前缀和后缀运算符(Prefix and Postifx Operators)
上个例子显示了一个双目中缀运算符的自定义实现.
类与结构体也能提供标准单目运算符的实现.
单目运算符只操作一个值.当运算符出现在值之前它就是前缀的(例如-a),而当它出现在值之后它就是后缀的(例如b!)
当声明运算符函数的时,你通过在func关键字之前指定prefix或者postfix修饰符来实现一个单目前缀运算符
<pre><code>prefix func - (vector: Vector2D) -> Vector2D { return Vector2D(x: -vector.x, y: -vector.y) }
</code></pre>
这段代码为Vector2D类型实现了单目负号运算符.
由于该运算符是前缀运算符,所以这个函数需要加上prefix修饰符.
对于简单数值,单目负号运算符可以对它们的正负性进行改变,对于Vector2D来说,该运算将其x和y属性的正负性都进行了改变:
<pre><code>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实例
</code></pre>
复合赋值运算符
复合赋值运算符将赋值运算符(=)与其它运算符进行结合.
例如,将加法与赋值结合成加法赋值运算符(+=).
在实现的时候,需要把运算符的左参数设置成inout类型,因为这个参数的值会在运算符函数内直接被修改.
<pre><code>func += (inout left: Vector2D, right: Vector2D) { left = left + right }
</code></pre>
因为加法运算在之前已经定义过了,所以在这里无需重新定义.加法赋值运算符函数可以直接利用现有的加法运算符函数,用它来对左值和右值进行相加,并再次赋值给左值:
<pre><code>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) print(original.x,original.y) //4.0 6.0
</code></pre>
注意
不能对默认的赋值运算符(=)进行重载
只有组合赋值运算符可以被重载
同样地,也无法对三目条件运算符(a ? b : c)进行重载
等价运算符(Equivalence Operators)
自定义的类和结构体没有获得对等价运算符进行默认实现,等价运算符通常被称为“相等”运算符(==)与“不等”运算符(!=).
对于自定义类型,Swift 无法判断其是否“相等”,因为“相等”的含义取决于这些自定义类型在你的代码中所扮演的角色.
为了使用等价运算符能对自定义的类型的等价进行检查,为其提供如同为其它中缀运算符那样的自定义实现:
<pre><code>func == (left: Vector2D, right: Vector2D) -> Bool { return (left.x == right.x) && (left.y == right.y) } func != (left: Vector2D, right: Vector2D) -> Bool { return !(left == right) }
</code></pre>
上述代码实现了“相等”运算符(==)来判断两个Vector2D实例是否相等.对Vector2D类型来说,“相等”意味着“两个实例的x属性和y属性都相等”,并且这也是“相等”运算符的实现所采用的逻辑.示例同时也实现了“不等”运算符(!=).它简单地将“相等”运算符的结果进行取反后返回.
现在我们可以使用这两个运算符来判断两个Vector2D实例是否相等:
<pre><code>`
var vector0 = Vector2D(x: 1.0, y: 2.0)
var vector1 = Vector2D(x: 1.0, y: 2.0)
if vector0 == vector1
{
print("These two vectors are equivalent.")
}
else
{
print("These two vectors are not equivalent.")
}
vector1 = Vector2D(x: 2.0, y: 3.0)
if vector0 == vector1
{
print("These two vectors are equivalent.")
}
else
{
print("These two vectors are not equivalent.")
}
//These two vectors are equivalent.
//These two vectors are not equivalent.
`</code></pre>
自定义运算符
除了Swift提供的标准运算符,你可以声明和实现自定义运算符.
可以用来自定义运算符的字符的列表请参考Operators.
新的运算符要使用operator关键字在全局作用域内进行定义,
同时还要用prefix,infix或者postfix修饰符标记:
<pre><code>prefix operator +++ {}
</code></pre>
上面的实例中定义了一个新的名为+++的前缀运算符.这个运算符在Swift中没有意义,因此我们对于Vector2D的实例来自定义它的意义.对这个示例,+++被视为“前缀翻倍”运算符.它使用了前面定义的复合加法运算符对自身进行相加,从而让Vector2D实例的x属性和y属性的值翻倍:
<pre><code>prefix func +++ (inout vector: 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) print(toBeDoubled) print(afterDoubling) //Vector2D(x: 2.0, y: 8.0) //Vector2D(x: 2.0, y: 8.0)
</code></pre>
自定义中缀运算符的优先级和结合性
自定义的中缀运算符也可以指定优先级和结合性.Precedence and Associativity中详细解释了这两个特性是如何影响自定义的中缀运算符与其他中缀运算符的关系.
结合性的可取值有left,right和none.当左结合运算符跟其他相同优先级的左结合运算符写在一起时,会跟左边的值进行结合.同理,当右结合运算符跟其他相同优先级的右结合运算符写在一起时,会跟右边的值进行结合.而非结合运算符不能跟其他相同优先级的运算符写在一起.
结合性的默认值是none,优先级的值是100.
以下例子定义了一个新的中缀运算符+-,此运算符的结合性为left,并且它的优先级为140:
<pre><code>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)
</code></pre>
这个运算符把两个向量的x值相加,同时用第一个向量的y值减去第二个向量的y值.因为它本质上是属于“相加型”运算符,所以将它的结合性和优先级被分别设置为left和140,这与+和-等默认的中缀“相加型”运算符是相同的.关于Swift标准库提供的运算符的结合性与优先级,参考Swift Standard Library Operators Reference
注意
当定义前缀与后缀运算符的时候,你不需要指定优先级.
如果对同一个被操作数同时使用前缀与后缀运算符,则后缀运算符会先参与运算.