Swift编程二十八(高级运算符)
案例代码下载
高级运算符
除了基本运算符中描述的运算符之外,Swift还提供了几个执行更复杂值操作的高级运算符。这些包括C和Objective-C中熟悉的所有按位和位移运算符。
与C中的算术运算符不同,Swift中的算术运算符默认不会溢出。溢出行为被捕获并报告为错误。要选择溢出行为,请使用Swift默认溢出的第二组算术运算符,例如溢出加法运算符(&+)。所有这些溢出运算符都以&符号开头。
定义自己的结构,类和枚举时,为这些自定义类型提供自己的标准Swift运算符实现会很有用。Swift可以轻松地为这些运算符提供定制的实现,并确切地确定它们对创建的每种类型的行为。
不仅限于预定义的运算符。Swift为提供了自定义中缀,前缀,后缀和赋值运算符的自由,具有自定义优先级和关联性。这些运算符可以像任何预定义的运算符一样在代码中使用和采用,甚至可以扩展现有类型以支持定义的自定义运算符。
按位运算符
按位运算符可以处理数据结构中的各个原始数据位。它们通常用于低级编程,例如图形编程和设备驱动程序创建。当使用来自外部源的原始数据时,按位运算符也很有用,例如编码和解码数据以通过自定义协议进行通信。
Swift支持C中的所有按位运算符,如下所述。
按位NOT运算符
该位NOT运算符(~)反转数所有位:
image
按位NOT运算符是一个前缀运算符,它出现在它操作的值之前,没有任何空格:
let initialBits: UInt8 = 0b00001111
let invertedBits = ~initialBits
UInt8整数有八位,可以存储0和255之间的任何值。此示例使用二进制值初始化一个UInt8整数,该二进制值00001111的前四位设置为0,其后四位设置为1。这相当于十进制值15。
然后使用按位NOT运算符创建一个新的常量invertedBits,该常量等于initialBits,但所有位都被反转。0成为1,1成为0。invertedBitsis的值11110000等于无符号十进制值240。
按位AND运算符
按位AND运算符(&)结合了两个数字的位数。它返回一个新的号码,仅当位两个输入数字在其位等于1才被设置为1:
image
在下面的例子中,firstSixBits和lastSixBits两个值具有四个中间位等于1。按位AND运算符将它们组合起来以产生数字00111100,该数字等于无符号十进制值60:
let firstSixBits: UInt8 = 0b11111100
let lastSixBits: UInt8 = 0b00111111
let middleFourBits = firstSixBits & lastSixBits
按位OR运算符
按位或运算符(|)对两个数的二进制位进行比较。运算符返回一个新数字,如果任一输入数的位等于1其位设置为1:
image
在下面的例子中,someBits和moreBits的值具有不同的位设置为1。按位OR运算符将它们组合起来以产生数字11111110,该数字等于无符号数254:
let someBits: UInt8 = 0b10110010
let moreBits: UInt8 = 0b01011110
let combinedbits = someBits | moreBits
按位异或运算符
的按位异或运算符,或“异或运算符”( ^),比较两个数的位。运算符返回一个新数字,输入位不同其位设置为1,输入位相同其位设置为0:
image
在下面的示例中,firstBits和otherBits一个值位为1,而另一个不是,按位异或运算符这个位设置1为其输出值。firstBits和otherBits所有其他位相同在输出值中设置为0:
let firstBits: UInt8 = 0b00010100
let otherBits: UInt8 = 0b00000101
let outputBits = firstBits ^ otherBits
按位左右移位运算符
在按位左移位运算符(<<)和按位右移位运算符(>>)中的数向左或向移动一个数的所有特定数量的位,根据下面定义的规则。
按位左右移位具有将整数乘以或除以因子2的效果。将整数位向左移动一个位置会使其值加倍,而将其向右移动一个位置会使其值减半。
无符号整数的移位行为
无符号整数的位移行为如下:
- 现有位按所请求的位数向左或向右移动。
- 移除超出整数存储边界的任何位都将被丢弃。
- 在原始位向左或向右移动之后,将零插入到留下的空间中。
这种方法被称为逻辑转换。
下图显示了11111111 << 1(11111111按位向左移动1)和11111111 >> 1(11111111按位向右移动1)的结果。移动蓝色数字,丢弃灰色数字,并插入橙色零:
image
以下是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写0xCC6699在Swift的十六进制数字表示中。然后通过按位AND运算符(&)和按位右移运算符(>>)将此颜色分解为其红色(CC),绿色(66)和蓝色(99)分量。
通过在数字0xCC6699和0xFF0000之间执行按位AND来获得红色分量。0xFF0000的0有效地“掩盖”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位数:
image
符号位是0(意思是“正”),七个值位是用二进制表示法写的数字4。
但是,负数以不同方式存储。它们的存储方式是将它们的绝对值减去2的n次方,其中n是值的位数。一个八比特数有七个值的位,所以这意味着2的7次方,即128。
以下是Int8查找数字的位数-4:
image
这一次,符号位是1(意思是“负”),七个值位的二进制值为124(即128 - 4):
image
负数的这种编码称为二进制补码表示。这似乎是一种代表负数的不寻常方式,但它有几个优点。
首先,可以-1加-4,简单地通过执行一个标准二进制加法全部八个位(包括符号位),并丢弃任何不适合8位,一旦完成:
image
其次,二进制补码表示还可以将负数位向左和向右移动,就像正数一样,并且对于向左移动的每一个移位最终都会将它们加倍,或者对于向右移动的每个位将它们减半。 。当有符号整数右移,适用无符号整数相同的规则,但在剩下的填充任何空位:要做到这一点,整数右移使用符号位补位,而不是零。
image
此操作可确保有符号整数在向右移动后具有相同的符号,并称为算术移位。
由于存储正数和负数的特殊方式,将它们中的任何一个向右移动都会使它们接近零。在此移位期间保持符号位相同意味着负整数向0移动保持为负值。
溢出运算符
如果尝试将数字插入到不能保存该值的整数常量或变量中,默认情况下Swift会报告错误,而不是允许创建无效值。当处理太大或太小的数字时,此行为可提供额外的安全性。
例如,Int16整数类型可以包含-32768和32767之间的任何有符号整数。尝试将Int16常量或变量设置为此范围之外的数字会导致错误:
var potentialOverflow = Int16.max
potentialOverflow += 1
当值变得太大或太小时提供错误处理,在编码边界值条件时提供更大的灵活性。
但是,如果特别希望溢出条件截断可用位数,则可以选择此行为而不是触发错误。Swift提供了三个算术溢出运算符,它们选择加入整数计算的溢出行为。这些运算符都以&符号开头&:
- 溢出加法(&+)
- 溢出减法(&-)
- 溢出乘法(&*)
值溢出
数字可以在正方向和负方向上溢出。
下面是使用overflow溢出加法(&+)允许无符号整数向正方向溢出时会发生什么的示例:
var unsignedOverflow = UInt8.max
unsignedOverflow = unsignedOverflow &+ 1
变量unsignedOverflow初始化为UInt8可以容纳的最大值(255或二进制11111111)。然后使用溢出加法运算符(&+)递增1。这使得它的二进制表示超出了UInt8可以容纳的大小,导致它溢出超出其边界,如下图所示。UInt8溢出添加后保留在范围内的值00000000为零。
image
当允许无符号整数向负方向溢出时会发生类似情况。这是使用溢出减法运算符(&-)的示例:
var unsignedOverflow = UInt8.min
unsignedOverflow = unsignedOverflow &- 1
UInt8可容纳的最小值为零或00000000二进制。如果使用溢出减法运算符(&-)从00000000减去1,这个数字将溢出并环绕到11111111,或十进制255。
image
对于有符号整数,也会发生溢出。有符号整数的所有加法和减法以按位方式执行,符号位作为加数或减数的一部分包括在内,如按位左右移位运算符中所述。
var signedOverflow = Int8.min
signedOverflow = signedOverflow &- 1
Int8最小值为-128或二进制10000000。使用溢出运算符从此二进制数中减去1得到二进制01111111,该值将切换符号位得出正数127,Int8即可容纳的最大正值。
image
对于有符号和无符号整数,正方向上的溢出从最大有效整数值回到最小值,而负方向上的溢出从最小值回到最大值。
优先级和相关性
运算符优先级使某些运算符的优先级高于其他, 那么首先计数优先级高的运算符。
运算符关联性定义了相同优先级的运算符如何组合在一起 - 从左侧分组,或从右侧分组。可以把它想象成“他们与左边的表达联系起来”或“他们将表达联系到他们的右边”。
在计算复合表达式的顺序时,考虑每个运算符的优先级和关联性非常重要。例如,运算符优先级解释了以下表达式等于17的原因。
2 + 3 % 4 * 5
如果从左到右严格阅读,可能表达式计算如下:
- 2加上3等于5
- 5余数4等于1
- 1时间5等于5
但是,实际答案17不是5。优先级较高的运算符在优先级较低的运算符之前进行求值。在Swift中,与在C中一样,余数运算符(%)和乘法运算符(*)的优先级高于加法运算符(+)。结果是在加法之前计数它们。
但是,余数和乘法具有相同的优先级。要得出确切计算顺序,还需要考虑它们的相关性。余数和乘法都与左边的表达式相关联。可以想象这是在表达式的这些部分周围添加隐式括号,从左边开始:
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类型的输入参数,并返回单个输出值,也是类型Vector2D。
在此实现中,输入参数被命名left和right表示将位于+运算符左侧和右侧的Vector2D实例。该方法返回一个新Vector2D实例,其x和y属性被从两个Vector2D实例的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
此示例将向量加在一起并生成向量,如下所示。(3.0, 1.0)(2.0, 4.0)(5.0, 5.0)
image
前缀和后缀运算符
上面显示的示例演示了二元中缀运算符的自定义实现。类和结构还可以提供标准一元运算符的实现。一元运算符在单个目标上运行。如果它们位于其目标(如)之前,则它们是前缀,如果它们在目标之后(例如-a),则它们是后缀运算符(如b!)。
通过在声明运算符方法时在func前写入关键字prefixor、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
let alsoPositive = -negative
复合赋值运算符
复合赋值运算符将(=)与另一个运算符相结合。例如,加法赋值运算符(+=)将加法和赋值组合到单个操作中。将复合赋值运算符的左输入参数类型标记为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
注意
无法重载默认赋值运算符(=)。只有复合赋值运算符才能重载。类似地,三元条件运算符(a ? b : c)不能重载。
等价运算符
默认情况下,自定义类和结构没有等价运算符的实现,称为等于运算符( == )和不等于 运算符( != )。通常实现运算符==,并使用标准库的运算符 != 来否定运算符 == 的结果的默认实现。有两种方法可以实现==运算符:可以自己实现它,或者对于许多类型,可以让Swift来实现。在这两种情况下,都可以遵守标准库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.")
}
在许多简单的情况下,可以让Swift为提供等价运算符的综合实现。Swift为以下类型的自定义类型提供了综合实现:
- 仅存储遵守Equatable协议的属性的结构
- 只包含遵守Equatable协议的关联类型的枚举
- 没有关联类型的枚举
要接收综合实现的==,在包含原始声明的文件中声明遵守Equatable,而不是自己实现==操作符。
下面的示例定义了三维位置矢量(x, y, z)的结构Vector3D,类似于结构Vector2D。因为x,y和z属性都是Equatable类型,所以接收综合的等价运营商的实现。
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.")
}
自定义运算符
除了Swift提供的标准运算符之外,还可以声明并实现自己的自定义运算符。有关可用于定义自定义运算符的字符列表,请参阅运算符。
新的运算符使用operator关键字全局声明,并且都标有prefix,infix或postfix修饰:
prefix operator +++
上面的示例定义了一个名为的新前缀运算符+++。此运算符在Swift中没有现有含义,因此在使用Vector2D实例的特定上下文中给出了它自己的自定义含义。出于此示例的目的,+++将其视为新的“前缀加倍”运算符。它通过使用前面定义的加法赋值运算符将向量与自身相加,使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
自定义中缀运算符的优先级
自定义中缀运算符均需指定优先级组。优先级组指定运算符相对于其他中缀运算符的优先级,以及运算符的关联性。有关这些特征如何影响中缀运算符与其他中缀运算符的交互的说明,请参阅优先级和关联性。
未明确放入优先级组的自定义中缀运算符将被赋予默认优先级组,其优先级即高于三元条件运算符的优先级。
以下示例定义了一个名为+-的新自定义中缀运算符,该运算符属于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
该运算符将两个向量的x值相加,并从第一个向量中减去第二个向量的y值。因为它本质上是一个“加法”运算符,所以它被赋予了与加法中缀运算符相同的优先级组,例如+和-。有关Swift标准库提供的运算符的信息,包括运算符优先级组和关联性设置的完整列表,请参阅运算符声明。有关优先级组的更多信息以及定义自己的运算符和优先级组的语法,请参阅“ 运算符声明”。
注意
定义前缀或后缀运算符时,不指定优先级。但是,如果将前缀和后缀运算符同时应用于同一操作数,则首先应用后缀运算符。