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的效果。将整数位向左移动一个位置会使其值加倍,而将其向右移动一个位置会使其值减半。

无符号整数的移位行为

无符号整数的位移行为如下:

  1. 现有位按所请求的位数向左或向右移动。
  2. 移除超出整数存储边界的任何位都将被丢弃。
  3. 在原始位向左或向右移动之后,将零插入到留下的空间中。

这种方法被称为逻辑转换。

下图显示了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标准库提供的运算符的信息,包括运算符优先级组和关联性设置的完整列表,请参阅运算符声明。有关优先级组的更多信息以及定义自己的运算符和优先级组的语法,请参阅“ 运算符声明”。

注意

定义前缀或后缀运算符时,不指定优先级。但是,如果将前缀和后缀运算符同时应用于同一操作数,则首先应用后缀运算符。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 205,033评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,725评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,473评论 0 338
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,846评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,848评论 5 368
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,691评论 1 282
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,053评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,700评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 42,856评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,676评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,787评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,430评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,034评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,990评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,218评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,174评论 2 352
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,526评论 2 343

推荐阅读更多精彩内容