高级操作符(Advanced Operators)

344 阅读20分钟

原文

除了描述在Basic Operators中的操作符,swift提供多个高级操作符,进行更复杂的之运算。这包括所有的你会从C和Objective-C中熟悉的按位和位转换运算符。

不像C中的运算符,swift中的运算符默认不会溢出。溢出表现会作为一个错误捕捉和报告。为了选择溢出的行为,使用swift的第二种默认是溢出的算术运算符,例如溢出加运算符(&+)。这些溢出运算符用一个ampersand(&)开始。

当你定义你自己的结构体,类,和枚举的时候,可能需要给这些自定义类型提供标准swift运算符的你自己的实现。swift使提供这些运算符的定制的实现和决定你创建的每种类型表现应该是什么样的变得很简单。

没有限制的你的预定义操作符。swift让你可以自由地定义自己自定义的前缀,中缀,后缀和分配操作符,用自定义的优先级和结合值。这些操作符可以像预定义的运算符一样在你的代码中使用和采用,你可以扩展已存在的类型来支持你自定义的操作符。

按位运算(Bitwise Operators)

按位操作符使你可以在数据结构体中操作各个原始数据位。他们经常用在底层的编程中,例如图形编程和设备硬件创建。按位运算符也可以在你使用外部数据的数据位的时候需要,例如为了通过自定义协议编码和解码数据。

swift支持所有的C中的按位运算符,像下面描述的。

按位否运算(Bitwise NOT Operator)

按位NOT运算符(~)把所有的为转换为一个数字:


按位NOT运算符是一个前缀操作符,立即出现在操作的值之前,没有空格:

let initialBits: UInt8 = 0b00001111
let invertedBits = ~initialBits  // equals 11110000

UInt8整型有八个位并且可以存储0到255之间任何的值。这个例子用00001111二进制初始化一个UInt8整型,前四位设置为0,后四位是1.他等于十进制15.

然后按位NOT操作符用来创建一个名为invertedBits的常量,等于initialBits,单全部的位转换了。零变成一,一变成零。invertedBits的值是11110000,等于无符号十进制240.

按位与运算(Bitwise AND Operator)

按位与(&)操作符结合两个数字的位。返回一个新的只有两个输入数字中的位都是1的时候它的为设置为1的数字:


下面的例子中,firstSixBits和lastSixBits的值都是有四个中间的位等于1.按位AND操作符把他们结合为数字00111100,等于无符号十进制60:

let firstSixBits: UInt8 = 0b11111100
let lastSixBits: UInt8  = 0b00111111
let middleFourBits = firstSixBits & lastSixBits  // equals 00111100

按位或运算(Bitwise OR Operator)

按位OR运算符(|)对比两个数字的位。操作符返回一个数字,它的位在两个输入数字有一个的位为1的时候设置为1:

下面的例子中,someBits的值和moreBits有不同的位设置为1.按位或操作符结合他们生成数字11111110,等于无符号十进制254:

let someBits: UInt8 = 0b10110010
let moreBits: UInt8 = 0b01011110
let combinedbits = someBits | moreBits  // equals 11111110

按位XOR运算(Bitwise XOR Operator)

按位XOR操作符,或者“异或操作符”(^),比较两个数字的位。运算符返回一个新的数字,它的位在输入的位不同的地方设置为1,输入的位相同的地方设置为0:


下面的例子中,每个firstBits和otherBits的值在另一个不是的位置设置为1.按位XOR运算符把输出值上的这些位设置为1。在firstBits和otherBits上的全部其他位匹配并且在输出值上设置为0:

let firstBits: UInt8 = 0b00010100
let otherBits: UInt8 = 0b00000101
let outputBits = firstBits ^ otherBits  // equals 00010001

按位向左向右转换运算(Bitwise Left and Right Shift Operators)

按位向左转换操作符和按位向右操作符把数字中全部的位向左或者向右移动一个确定的数字,遵循下面定义的规则。

按位向左和按位向右转换的效果是将一个整数乘以或者除以一个因子2.将整型的位向左移动一个位置将它的值翻倍,将它向右移动一个位置是把他的值减半。

无符号整型转换表现(Shifting Behavior for Unsigned integers)

对无符号整型移动的行为像下面一样:

  1. 已存在的位向左或者向右移动请求放置的数字。
  2. 任何移动到整型存储边界之外的位会被丢弃。
  3. 插在原始位左侧之后的位置的0移动到左边或者右边

这种方法称为逻辑转移。

下面的图片展示了11111111 << 1的结果(11111111向左移动1个位置),和11111111 >> 1(11111111向右移动1个位置)。蓝色数字是移动的,灰色数字是丢弃的,橙色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

这个例子用一个名为pink的UInt32常量来给粉色存储一个Cascading Style Sheets颜色值。CSS颜色值#CC6699在swift的16进制数字表示中写作0xCC6699。然后这个颜色通过按位与(&)和按位向右移动(>>)分解为red(CC),green(66),和蓝色(99)部分。

red成分通过进行在数字0xCC6699和0xFF0000之间进行一个按位AND运算获得。0xFF0000中的0有效的遮盖了0xCC6699的第二和第三字节,使6699被忽略并且作为结果留下了0xCC0000。

然后这个数字向右移动16位(>>16)。16进制数字中的每对字符使用8位,所以向右移动16位会吧0xCC0000转换为0x0000CC。这和0xCC一样,十进制的值位204.

相似的,绿色部分通过进行在0xCC6699和0x00FF00之间的按位与运算,得到一个0x006600的输出值。这个输出值之后向右移动8位,得到一个0x66的值,十进制的值为102.

最后,蓝色部分通过对数字0xCC6699和0x0000FF的值进行按位AND,输出值0x000099.不需要向右移动,因为0x000099等于0x99,十进制值位153.

带符号整型移位(Shifting Behavior for Signed Integers)

移动带符号的整型比不带符号的复杂很多,因为带符号整型的表示用二进制。(下面的例子为了简单在8位有符号整型的基础上,但是相同的原则用于任何尺寸的带符号整型)。

带符号整型使用它们的第一位(称为符号位)来指明整型时证书还是负数。0符号位表示正数,1符号位表示负数。

剩下的为(称为值位)存储实际的值。正数和无符号整型一样的方式存储,从零开始计数。这里是数字4的Int8中位的样子:


符号位为0(表示负数),七个值位就是数字4,用二进制法表示。

负数,不过,存储不一样。它们的存储方法是将绝对值从2的n次方减去,n是值位数的数字。一个八位数字有七个值位,所以这意味着2的七次方,或者128.

这里是数字-4的Int8中位的样子:


这次,符号位时1(意味着“负数”),七个值位有一个124的二进制值(是128-4):


这个对负数的编码被称为2的补码表示。这看起来是一个表示负数的不正常的方式,但他有很多优点。

首先,可以简单地通过进行一个标准的全部八位加法,把-1加到-4上(包括符号位),一旦完成了丢弃了任何在八位中不适合的东西:


第二点,二的补码也可以让你像正数一样把负数的位向左或者向右移动,对你向左的每次移动仍然使他们翻倍,或者每次向右移动把它们减半。为了实现这些,当有符号整型向右移动的时候有一个额外的规则:当你将有符号整型向右移动的时候,使用和无符号一样的规则,但是在左边空的位添加一个符号位,而不是一个0.


这个动作确保在他们向右移动之后符号整形有一样的符号,称为算术移动。

因为帧数和负数存储的特殊方式,他们向右的移位使他们更接近0。在移动期间保持符号位不变意味着负数整形在他们的值靠近0的时候保持负数。

溢出操作符(Overflow Operators)

如果你尝试在不能容纳那个值的常量或者变量中插入一个数字,swift默认报错而不是允许创建无效的值。当你用太大或者太小的数字的时候提供了额外的安全。

例如,Int16整型类型可以存储-32768和32767之间的数字。尝试给把一个Int16的常量或者变量设置为超过范围的数字会导致一个错误:

var potentialOverflow = Int16.max
// potentialOverflow equals 32767, which is the maximum value an Int16 can hold
potentialOverflow += 1
// this causes an error

当值变得太大或者太小的时候提供错误处理给你在编写边界值条件的时候提供更多的灵活。

不过,在你想用溢出状态来截取可获取的位的数字的时候,可以选择这个行为,胃不适触发错误。swift提供三个算术溢出符,为整型计算选择溢出表现。这些操作符都以(&)喀什:

  • Overflow addition (&+)
  • Overflow subtraction (&-)
  • Overflow multiplication (&*)

值溢出(Value Overflow)

数字在正数和负数两个方向都可以溢出。

这是一个在无符号整型允许在正值上溢出的时候发生的,使用溢出加操作符(&+):

var unsignedOverflow = UInt8.max
// unsignedOverflow equals 255, which is the maximum value a UInt8 can hold
unsignedOverflow = unsignedOverflow &+ 1
// unsignedOverflow is now equal to 0

变量unsignedOverflow用一个UInt8能保存的最大值来初始化(255,或者11111111二进制)。然后通过溢出加符号增加1.这是他的二进制表示超过了UInt8能表示的尺寸,导致他超出了它的边界,像下面图片展示的。在溢出加之后UInt8边界中留下的值时00000000,或者0.


当允许无符号整型在负数方向上溢出的时候发生相似的现象。这里是使用溢出减符号的例子(&-):

var unsignedOverflow = UInt8.min
// unsignedOverflow equals 0, which is the minimum value a UInt8 can hold
unsignedOverflow = unsignedOverflow &- 1
// unsignedOverflow is now equal to 255

UInt8能保存的最小的值是0,或者00000000二进制。如果从000000减一的话使用溢出减法操作符(&-),数字会溢出并且截断为1111111,或者十进制255.


溢出也发生在有符号整型。对于有符号整型的全部加法和减法用按位方式进行,在被加或者被减的数字中包括符号位,像Bitwise Left and Right Shift Operators中描述的。

var signedOverflow = Int8.min
// signedOverflow equals -128, which is the minimum value an Int8 can hold
signedOverflow = signedOverflow &- 1
// signedOverflow is now equal to 127

Int8能保存的最小的值是-128,或者10000000二进制。这个二进制用溢出操作符减1会得到01111111的二进制值,切换符号位并给了127的正值,是Int8能保存的最大的正值。


对于两个有符号和无符号的整型,在正值方向的溢出把最大有效整型值变回了最小的值,在负方向的溢出把最小的值变回了最大的。

优先与结合(Precedence and Associativity)

操作符优先级给一些操作符比其他操作符更高的权限;这些操作符先运用。操作符结合律定义了一样等级的操作符如何组合在一起--从左来使组合,或者从右开始。把它当做“他们把表达式关联到他们左边”,或者“他们把表达式关联它他们右边”。

考虑每个操作符的优先级操作符的优先级和结合律非常重要,当在计算将要执行的组合表达式的顺序的时候。例如,操作符的优先级解释了为什么下面结果是17。

2 + 3 % 4 * 5
// this equals 17

如果直接从左到右读,可能期望的表达式计算结果是这样:

  • 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标准库提供的操作符的信息,包括完整的操作符优先级组合和结合设置的列表,查看Operator Declarations

swift的操作符优先级和关联规则比在C和Objective-C中的更简单和可预测。不过,这意味着他们和C基础的语言不完全一样。当把已存在的代码导入到swift中的时候小心确保操作符交互表现和你期望的仍然一样。

操作符方法(Operator Methods)

类和结构体可以提供他们自己的已存在的操作符的实现。这就是重写已存在的操作符。

下面的例子展示了如何为自定义结构体实现算术加法。算术加法操作符是一个二进制运算符因为它操作两个目标并且中缀的,因为他出现在这两个目标之间。

例子定义了一个Vector2D结构体为一个二维位置向量(x,y),跟着一个操作方法的定义来把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
// combinedVector is a Vector2D instance with values of (5.0, 5.0)

这个例子吧vectors(3.0,1.0)和(2.0,4.0)加在一起来获取(5.0,5.0)向量,像下面展示的。


前缀与后缀操作符(Prefix and Postfix Operators)

上面的例子解释了一个自定义的二元中间运算符的实现。类和结构体也可以提供标准一元元素安抚的实现。一元运算符在单个目标上操作。如果他们比他们目标之前则是前缀(例如-a),如果跟在目标后面则是后缀(例如b!)。

通过在声明运算符方法的时候在func关键字之前写prefix或者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
// 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)

组合分配操作将分配符和另一个操作符合并。例如,加法分配符(+=)将加号和分配结合到一个单一的操作符中。把组合分配符的左侧输入参数类型标记为input,因为参数的值会在操作方法中直接修改。

下面的例子为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
// original now has values of (4.0, 6.0)

不能重写默认的分配操作符。只有组合分配符可以被重写的时候。相似的,三元条件操作符不能被重写。

相等操作符(Equivalence Operators)

默认的,自定义类和结构体没有恒等于操作符的实现,像等于符号(==)和不等于符号(!=)。通常实现==操作符,并且使用标准库的默认的!=实现,反转==操作符的结果。有两个方式实现==操作符:可以自己实现它,或者为多种类型,你可以请求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.")
}
// Prints "These two vectors are equivalent."

许多简单的情况中,可以要求swift为你提供相等操作符的合成实现。swift为下面自定义类型种类提供合成实现:

  • 只有遵循Equatable协议的存储属性的结构体
  • 只有遵循Equatable协议的相关联类型的枚举
  • 没有相关联类型的枚举

获得一个==的合成实现,在包含原始声明的文件中声明Equatable的遵循,不用自己实现==操作符。

下面的例子为三维位置向量(x,y,z)定义了一个Vector3D结构体,与Vector2D结构体相似。因为x,y,和z属性是一个Equatable类型的全部属性,Vector3D接受相等操作符的合成实现。

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.")
}
// Prints "These two vectors are also equivalent."

自定义操作符(Custom Operators)

除了swift中的标准操作符你可以声明和实现自己的自定义操作符。关于可以用来定义自定义操作符的符号列表,查看Operators

新的操作符在全局层级用Operator关键字来声明,用prefix,infix或者postfix修饰词标记:

prefix operator +++

上面的例子定义了一个新的前缀操作符名为+++。在swift中操作符没有已存在的意义,所以在下面用Vector2D实例的特别的环境中给了她自定义的意义。对于这个例子的目的,+++作为一个新的”双前缀“操作符。他爸一个Vector2D实例的x和y的值翻倍,通过用之前定义的加法分配操作符给他自己增加向量。要实现+++操作符,在下面增加一个名为+++的类型方法:

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
// toBeDoubled now has values of (2.0, 8.0)
// afterDoubling also has values of (2.0, 8.0)

自定义中缀操作符优先级(Precedence for Custom Infix Operators)

每个自定义中间操作符属于一个优先级族。一个优先级组指定和其他中间操作符关联的一个操作符优先级,和操作符的分配率。查看Precedence and Associativity关于这些特性如何影响中间操作符和其他中间操作符的交互的说明。

一个没有明确放到优先级组中的自定义中间操作符默认有一个优先级高于三元条件操作符的优先级数组。

下面的例子定义了名为+-的一个新的自定义的中间操作符,属于优先级组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
// plusMinusVector is a Vector2D instance with values of (4.0, -2.0)

操作符把两个vectors的x值加在一起,从第一个中减去第二个vector的y值。因为它本质上是一个加法操作符,给了它和加法中级操作符例如+和-一样的优先级组,包括一个完整的优先级操作符数组和关联设置,查看Operator Declarations。更多关于优先级数组的信息,了解定义自己操作符的语法和优先级数组,查看Operator Declaration

当定义一个前缀或者后缀操作符的时候不指定优先级。不过,如果你对一样的相同的操作数使用前缀和后缀操作符,先用后缀操作符。