翻译自:https://docs.swift.org/swift-book/LanguageGuide/AdvancedOperators.html
除了基本运算符中描述的运算符外,Swift还提供了几个高级运算符来执行更复杂的值操作。这些包括您将熟悉的C和Objective-C的所有位和位移位运算符。
与C中的算术运算符不同,Swift中的算术运算符默认不会溢出。溢出行为被困住,并报告为错误。要选择溢出行为,请使用Swift的第二组默认溢出的算术运算符,例如溢出加法运算符(&+
)。所有这些溢出运算符都以安培和(&
)开头。
当您定义自己的结构、类和枚举时,为这些自定义类型提供您自己的标准Swift运算符的实现可能会很有用。Swift可以轻松提供这些运算符的定制实现,并准确确定它们对您创建的每个类型的行为。
您不限于预定义的运算符。Swift允许您自由定义自己的自定义内缀、前缀、后缀和赋值运算符,并具有自定义优先级和关联性值。这些运算符可以像任何预定义运算符一样在您的代码中使用和采用,您甚至可以扩展现有类型以支持您定义的自定义运算符。
按位运算符使您能够操作数据结构中的单个原始数据位。它们通常用于低级编程,例如图形编程和设备驱动程序创建。当您处理来自外部来源的原始数据时,例如编码和解码数据以通过自定义协议进行通信时,按位运算符也很有用。
Swift支持C中的所有按位运算符,如下所述。
按位不算符(~
)反转数字中的所有位:
按位NOt运算符是一个前缀运算符,并显示在其操作的值之前,没有任何空格:
UInt8
整数有8位,可以存储0
到255
之间的任何值。此示例初始化二进制值00001111
的UInt8
整数,其前四位设置为0
,后四位设置为1。这相当于小数点后15。
然后,按位NOt运算符创建一个名为invertedBits
的新常量,该常量等于initialBits,但所有位都倒置。零变成1,1变成零。invertedBits
的值为11110000
,等于240
的无符号小数值。
按位和运算符(&
)结合了两个数字的位。只有当两个输入数字中的位等于1
时,它才会返回一个新数字,其位设置为1
:
在下面的示例中,firstSixBits
和lastSixBits
的值都有四个中间位等于1。按位和运算符将它们组合成数字00111100
,等于60
的无符号小数值:
按位OR运算符(|
)比较两个数字的位。如果任一输入号中的位等于1
运算符返回一个新数字,其位设置为1
:
在下面的示例中,someBits
和moreBits
的值将不同的位设置为1。按位或运算符将它们组合成数字11111110
,等于254
的无符号小数点:
按位XOR运算符,或“排他性OR运算符”(^
),比较两个数字的位。运算符返回一个新数字,其位设置为1
,其中输入位不同,并设置为0
,其中输入位相同:
在下面的示例中,firstBits
和otherBits
的值在另一个没有的位置上都设置为1
。按位XOR运算符将这两个位的输出值设置为1
。firstBits``otherBits
中的所有其他位都匹配,并在输出值中设置为0
:
根据下面定义的规则,按位左移运算符(<<
)和按位右移运算符(>>
)将数字中的所有位向左或向右移动一定数量的位置。
位左移和右移具有整数乘以或除以二倍的效果。将整数的位向左移动一个位置会使其值翻倍,而将其向右移动一个位置会将其值减半。
无符号整数的位移位行为如下:
这种方法被称为逻辑转变。
The illustration below shows the results of 11111111 << 1
(which is 11111111
shifted to the left by 1
place), and 11111111 >> 1
(which is 11111111
shifted to the right by 1
place). Blue numbers are shifted, gray numbers are discarded, and orange zeros are inserted:
以下是 Swift 代码中位移的外观:
您可以使用位移来编码和解码其他数据类型中的值:
此示例使用名为pink
的UInt32
常量来存储粉红色的级联样式表颜色值。CSS颜色值#CC6699
在Swift的十六进制数字表示中写为0xCC6699
。然后,按位AND运算符(&
)和按位右移运算符(>>
)分解为红色(CC
)、绿色(66
)和蓝色(99
)组件。
红色分量是通过在数字0xCC6699
和0xFF0000
之间执行按位AND获得的。0xFF0000
中的零有效地“屏蔽”了0xCC6699
的第二和第三个字节,导致6699
被忽略,并因此留下0xCC0000
。
This number is then shifted 16 places to the right (>> 16
). Each pair of characters in a hexadecimal number uses 8 bits, so a move 16 places to the right will convert 0xCC0000
into 0x0000CC
. This is the same as 0xCC
, which has a decimal value of 204
.
同样,绿色分量是通过在数字0xCC6699
和0x00FF00
之间执行按位AND获得的,输出值为0x006600
。然后,该输出值向右移动八个位置,给出的值为0x66
,小数值为102
。
最后,通过在数字0xCC6699
和0x0000FF
之间执行按位AND获得蓝色分量,输出值为0x000099
。由于0x000099
已经等于0x99
,其小数值为153,因此使用此值时不会将其向右移动,
有符号整数的移位行为比无符号整数更复杂,因为有符号整数在二进制中表示的方式。(为了简单起见,以下示例基于8位有符号整数,但同样的原则适用于任何大小的有符号整数。)
有符号整数使用它们的第一个位(称为符号位)来指示整数是正数还是负数。0
的符号位表示正值,1
的符号位表示负数。
剩余的位(称为值位)存储实际值。正数的存储方式与无符号整数完全相同,从0
向上计数。以下是Int8
中的位如何查找数字4
:
符号位为0
(意为“正”),七个值位只是数字4,用二进制符号书写。
然而,负数的存储方式不同。它们通过从2
减去n
的绝对值来存储,其中n
是值位数。八位数字有7个值位,这意味着2
到7或128
的功率。
以下是Int8
内部的位如何查找数字-4
:
这一次,符号位为1
(意为“负”),七个值位的二进制值为124
(即1284
):
这种负数编码被称为二的补数表示。这可能看起来是一种不寻常的表示负数的方式,但它有几个优点。
首先,您可以添加-1
到-4
,只需对所有8位(包括符号位)进行标准二进制添加,并在完成后丢弃任何不适合8位的东西:
其次,两者的补码表示还允许您像正数一样将负数位移到左侧和右侧,并且最终在向左移动的每移动时将其翻倍,或者在向右移动的每移动时将其减半。为了实现这一目标,当有符号整数向右移动时,会使用额外的规则:当您向右移动有符号整数时,请应用与无符号整数相同的规则,但用符号位而不是用零填充左侧的任何空位。
此操作确保有符号整数在向右移动后具有相同的符号,并被称为算术移位。
由于正数和负数的存储方式特殊,将它们中的任何一个移动到右边会使它们接近于零。在这种转变期间保持符号位不变意味着负整数在值接近于零时保持负数。
如果您尝试将数字插入无法保存该值的整数常量或变量中,默认情况下,Swift会报告错误,而不是允许创建无效值。当您处理太大或太小的数字时,这种行为会带来额外的安全性。
例如,Int16
整数类型可以保存-32768
和32767
之间的任何有符号整数。尝试将Int16
常量或变量设置为此范围之外的数字会导致错误:
当值太大或太小时提供错误处理,使您在编码边界值条件时具有更大的灵活性。
但是,当您特别希望溢出条件截断可用位数时,您可以选择此行为,而不是触发错误。Swift提供了三个算法溢出运算符,这些运算符选择溢出行为进行整数计算。这些运算符都以安培数(&
)开头:
&+
)&-
)&*
)数字可以向正向和负方向溢出。
以下是一个示例,说明当允许无符号整数使用溢出加法运算符(&+
)向正方向溢出时会发生什么:
变量unsignedOverflow
初始化为UInt8
可以持有的最大值(255
,二进制为11111111
)。然后使用溢出加法运算符(&+
)将其增加1
。这使其二进制表示略高于UInt8
可以容纳的大小,导致其溢出超出其界限,如下图所示。溢出加法后保持在UInt8
范围内的值为00000000
或零。
当允许无符号整数向负方向溢出时,也会发生类似的事情。以下是使用溢出减法运算符(&-
)的示例:
UInt8
可以持有的最低值为零,或二进制中的00000000
。如果您使用溢出减法运算符(&-
)从00000000
中减去1
,该数字将溢出并包装为11111111
,或小数255
。
签名整数也会发生溢出。有符号整数的所有加法和减法都以按位方式执行,符号位包含在数字中添加或减去中,如按位左移和右移运算符中所述。
Int8
可以持有的最低值为-128
,或二进制中的10000000
。使用溢出运算符从这个二进制数中减去1
,二进制值为01111111
,这会切换符号位并给出正127
,即Int8
可以持有的最大正值。
对于有符号整数和非有符号整数,正方向的溢出从最大有效整数值回最小值,负方向的溢出从最小值到最大值。
运算符优先级赋予一些运算符比其他运算符更高的优先级;这些运算符首先应用。
运算符结合性定义了具有相同优先级的运算符如何分组在一起——要么从左分组,要么从右分组。把它想象成“他们与左边的表达式相关联想”,或“他们与右边的表达式相关联”。
在计算复合表达式的顺序时,重要的是要考虑每个算子的优先级和关联性。例如,运算符优先级解释了为什么以下表达式等于17。
如果您严格从左到右阅读,您可能会期望表达式计算如下:
2
加3
等于5
5
剩余的4
等于1
1
乘以5
等于5
然而,实际答案是17,而不是5。高优先级算子在低优先级运算符之前进行评估。在Swift中,与C一样,余数运算符(%
)和乘法运算符(*
)的优先级高于加法运算符(+
)。因此,在考虑添加之前,它们都会被评估。
然而,余数和乘法具有相同的优先级。要确定要使用的确切评估顺序,您还需要考虑它们的关联性。剩余和乘法都与左边的表达式相关联。将其视为从左侧开始,在表达式的这些部分周围添加隐式括号:
(3 % 4)
是3,所以这相当于:
(3 * 5)
是15,所以这相当于:
这一计算得出了17的最终答案。
有关Swift标准库提供的运算符的信息,包括运算符优先级组和关联性设置的完整列表,请参阅运算符声明。
注意
Swift的运算符优先级和结合性规则比C和Objective-C更简单、更可预测。然而,这意味着它们与基于C的语言并不完全相同。在将现有代码移植到Swift时,请务必确保运营商交互的行为仍然像您希望的方式。
类和结构可以提供现有运算符自己的实现。这被称为使现有运算符超载。
下面的示例展示了如何为自定义结构实现算术加法运算符(+
)。算术加法运算符是一个二进制运算符,因为它在两个目标上运行,而它是一个内缀运算符,因为它出现在这两个目标之间。
该示例为二维位置向量(x,y)
定义了Vector2D
结构,然后是将Vector2D
结构实例相加的运算符方法的定义:
运算符方法被定义为Vector2D
上的类型方法,其方法名称与要重载的运算符(+
)匹配。由于加法不是向量基本行为的一部分,因此类型方法在Vector2D
的扩展中定义,而不是在Vector2D
的主结构声明中定义。由于算术加法运算符是二进制运算符,因此该运算符方法接受Vector2D
类型的两个输入参数,并返回一个输出值,也是Vector2D
类型的输出值。
在这个实现中,输入参数被命名为left
和right
,以表示位于+
运算符左侧和右侧的Vector2D
实例。该方法返回一个新的Vector2D
实例,其x
和y
属性使用添加到在一起的两个Vector2D
实例的x
和y
属性的总和初始化。
类型方法可以用作现有Vector2D
实例之间的修复运算符:
此示例将矢量(3.0,1.0)
和(2.0,4.0)
组合在一起,使矢量(5.0,5.0)
如下所示。
上面显示的示例演示了二进制修复运算符的自定义实现。类和结构还可以提供标准一元运算符的实现。单一运算符在单个目标上运行。如果它们在目标(如-a
)之前,它们是前缀,如果他们遵循目标(如b!
则为后缀运算符。
在声明运算符方法时,您可以通过在func
关键字之前写入prefix
或postfix
修饰符来实现前缀或后缀一元运算符:
上面的示例实现了Vector2D
实例的一元减运算符(-a
)。一元减算符是前缀运算符,因此这种方法必须用prefix
修饰符限定。
对于简单的数值,一元减算符将正数转换为负等价数,反之亦然。Vector2D
实例的相应实现对x
和y
属性执行此操作:
复合赋值运算符将赋值(=)与另一个运算相结合。例如,加法赋值运算符(+=
将加法和赋值组合成一个运算。您可以将复合赋值运算符的左输入参数类型标记为inout
,因为参数的值将直接从运算符方法中修改。
以下示例实现了Vector2D
实例的加法赋值运算符方法:
由于添加运算符是早些时候定义的,因此您无需在这里重新实现添加过程。相反,加法赋值运算符方法利用了现有的加法运算符方法,并使左值设置为左值加右值:
注意
It isn’t possible to overload the default assignment operator (=
). Only the compound assignment operators can be overloaded. Similarly, the ternary conditional operator (a ? b : c
) can’t be overloaded.
默认情况下,自定义类和结构没有等价运算符的实现,称为等于运算符(==
,不等于运算符(!=
)。您通常实现==
运算符,并使用标准库的默认实现!=
否定==
运算符结果的运算符。有两种方法可以实现==
运算符:您可以自己实现它,或者对于许多类型,您可以让Swift为您合成实现。在这两种情况下,您都会添加与标准库的Equatable
协议的一致性。
您以与实现其他修复运算符相同的方式提供==
运算符的实现:
上面的示例实现了==
运算符来检查两个Vector2D
实例是否具有等效值。在Vector2D
的上下文中,将“相等”视为“这两个实例具有相同的x
值和y
值”是有道理的,因此这是运算符实现使用的逻辑。
您现在可以使用此运算符检查两个Vector2D
实例是否等效:
在许多简单的情况下,您可以要求Swift为您提供等效运算符的合成实现,如《采用使用合成实现的协议》中所述。
除了Swift提供的标准运算符外,您还可以声明和实现自己的自定义运算符。有关可用于定义自定义运算符的字符列表,请参阅运算符。
新运算符使用operator
关键字在全局级别声明,并标有prefix
、infix
或postfix
修饰符:
上面的示例定义了一个名为+++
的新前缀运算符。此运算符在Swift中没有现有含义,因此在使用Vector2D
实例的特定上下文中,它在下面被赋予了自己的自定义含义。在本例中,+++
被视为一个新的“前缀加倍”运算符。它通过使用前面定义的加法赋值运算符将向量添加到自身,将Vector2D
实例的x
和y
值翻倍。要实现+++
运算符,请在Vector2D
中添加一个名为+++
的类型方法,如下所示:
每个自定义修复运算符都属于优先级组。优先级组指定运算符相对于其他内缀运算符的优先级,以及运算符的关联性。有关这些特征如何影响内缀运算符与其他内缀运算符的交互的说明,请参阅优先级和关联性。
没有显式放置在优先级组中的自定义内缀运算符将获得一个默认优先级组,其优先级直接高于三元条件运算符的优先级。
以下示例定义了一个名为+-
的新自定义内缀运算符,该运算符属于优先级组 AdditionPrecedence
:
这个运算符将两个向量的x
值加在一起,并从第一个向量中减去第二个向量的y
值。因为它本质上是一个“加法”运算符,所以它被赋予了与+
和-
等加性内缀运算符相同的优先级组。有关Swift标准库提供的运算符的信息,包括运算符优先级组和关联性设置的完整列表,请参阅运算符声明。有关优先级组的更多信息,以及查看定义您自己的运算符和优先级组的语法,请参阅运算符声明。
注意
在定义前缀或后缀运算符时,您不会指定优先级。但是,如果您同时将前缀和后缀运算符应用于同一操作数,则首先应用后缀运算符。
结果生成器是您定义的一种类型,它以自然、声明的方式添加用于创建嵌套数据(如列表或树)的语法。使用结果构建器的代码可以包括普通的Swift语法,例如if
和for
,以处理条件或重复的数据。
以下代码定义了使用星星和文本在单行上绘制的几种类型。
Drawable
协议定义了对可以绘制的东西的要求,例如线条或形状:类型必须实现draw()
方法。Line
结构代表单线绘图,它为大多数绘图的顶层容器服务。要绘制一条Line
,结构在每行的组件上调用draw()
,然后将生成的字符串连接成单个字符串。Text
结构包裹字符串使其成为绘图的一部分。AllCaps
结构包装和修改另一张绘图,将绘图中的任何文本转换为大写。
可以通过调用初始化器来使用这些类型绘制绘图:
这个代码有效,但有点尴尬。AllCaps
之后的深嵌套括号很难阅读。当name
为nil
使用“世界”的后备逻辑必须使用??
完成操作员,如果更复杂,那就很难了。如果您需要包含开关或循环来构建部分绘图,则无法做到这一点。结果生成器允许您像这样重写代码,使其看起来像普通的Swift代码。
要定义结果构建器,请在类型声明上写入@resultBuilder
属性。例如,此代码定义了一个名为DrawingBuilder
的结果构建器,它允许您使用声明语法来描述绘图:
DrawingBuilder
结构定义了实现结果构建器语法部分的三种方法。buildBlock(_:)
方法增加了对在代码块中写入一系列行的支持。它将该块中的组件组合成一条Line
。ThebuildEitherbuildEither(first:)
和buildEither(second:)
方法增加了对if
-else
的支持。
您可以将@DrawingBuilder
属性应用于函数的参数,该参数将传递给函数的闭包转换为结果构建器从该闭包创建的值。例如:
makeGreeting(for:)
函数使用name
参数,并用它来绘制个性化的问候语。draw(_:)
和caps(_:)
函数都以单个闭包作为参数,该闭包标有@DrawingBuilder
属性。当您调用这些函数时,您使用DrawingBuilder
定义的特殊语法。Swift将绘图的声明性描述转换为对DrawingBuilder
上方法的一系列调用,以建立作为函数参数传递的值。例如,Swift将该示例中的对caps(_:)
调用转换为以下代码:
Swift将if
-else
块转换为对buildEither(first:)
和buildEither(second:)
方法的调用。虽然您不会在自己的代码中调用这些方法,但当您使用DrawingBuilder
语法时,显示转换结果可以更容易地查看Swift如何转换代码。
要在特殊绘图语法中添加for
循环写入的支持,请添加buildArray(_:)
方法。
在上面的代码中,for
循环创建一个绘图数组,buildArray(_:)
方法将该数组转换为Line
。
有关Swift如何将构建器语法转换为对构建器类型方法的调用的完整列表,请参阅结果构建器。