优先级和结合性
运算符的优先级使得一些运算符优先于其他运算符;它们会先被执行。
结合性定义了相同优先级的运算符是如何结合的,也就是说,是与左边结合为一组,还是与右边结合为一组。可以将其理解为“它们是与左边的表达式结合的”,或者“它们是与右边的表达式结合的”。
当考虑一个复合表达式的计算顺序时,运算符的优先级和结合性是非常重要的。举例来说,运算符优先级解释了为什么下面这个表达式的运算结果会是 17
。
2 + 3 % 4 * 5 // 结果是 17
如果你直接从左到右进行运算,你可能认为运算的过程是这样的:
- 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 标准库提供的操作符信息,包括操作符优先级组和结合性设置的完整列表,请参见 操作符声明。
注意
相对 C 语言和 Objective-C 来说,Swift 的运算符优先级和结合性规则更加简洁和可预测。但是,这也意味着它们相较于 C 语言及其衍生语言并不是完全一致。在对现有的代码进行移植的时候,要注意确保运算符的行为仍然符合你的预期。
运算符函数
类和结构体可以为现有的运算符提供自定义的实现。这通常被称为运算符 重载 。
下面的例子展示了如何让自定义的结构体支持加法运算符(+
)。算术加法运算符是一个二元运算符,因为它是对两个值进行运算,同时它还可以称为中缀运算符,因为它出现在两个值中间。
例子中定义了一个名为 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
分别等于作为参数的两个实例的 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 是一个新的 Vector2D 实例,值为 (5.0, 5.0)
这个例子实现两个向量 (3.0,1.0)
和 (2.0,4.0)
的相加,并得到新的向量 (5.0,5.0)
前缀和后缀运算符
上个例子演示了一个二元中缀运算符的自定义实现。类与结构体也能提供标准一元运算符的实现。一元运算符只运算一个值。当运算符出现在值之前时,它就是前缀的(例如 -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 是一个值为 (-3.0, -4.0) 的 Vector2D 实例 let alsoPositive = -negative // alsoPositive 是一个值为 (3.0, 4.0) 的 Vector2D 实例
复合赋值运算符
复合赋值运算符将赋值运算符(=
)与其它运算符进行结合。例如,将加法与赋值结合成加法赋值运算符(+=
)。在实现的时候,需要把运算符的左参数设置成 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 // original 的值现在为 (4.0, 6.0)
注意
不能对默认的赋值运算符(
=
)进行重载。只有复合赋值运算符可以被重载。同样地,也无法对三元条件运算符 (a ? b : c
) 进行重载。
等价运算符
通常情况下,自定义的类和结构体没有对等价运算符进行默认实现,等价运算符通常被称为相等运算符(==
)与不等运算符(!=
)。
为了使用等价运算符对自定义的类型进行判等运算,需要为“相等”运算符提供自定义实现,实现的方法与其它中缀运算符一样, 并且增加对标准库 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.") } // 打印“These two vectors are equivalent.”
多数简单情况下,你可以让 Swift 合成等价运算符的实现,详见 使用合成实现来采纳协议。
自定义运算符
除了实现标准运算符,在 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 // toBeDoubled 现在的值为 (2.0, 8.0) // afterDoubling 现在的值也为 (2.0, 8.0)
自定义中缀运算符的优先级
每个自定义中缀运算符都属于某个优先级组。优先级组指定了这个运算符相对于其他中缀运算符的优先级和结合性。优先级和结合性 中详细阐述了这两个特性是如何对中缀运算符的运算产生影响的。
而没有明确放入某个优先级组的自定义中缀运算符将会被放到一个默认的优先级组内,其优先级高于三元运算符。
以下例子定义了一个新的自定义中缀运算符 +-
,此运算符属于 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 是一个 Vector2D 实例,并且它的值为 (4.0, -2.0)
这个运算符把两个向量的 x
值相加,同时从第一个向量的 y
中减去第二个向量的 y
。因为它本质上是属于“相加型”运算符,所以将它放置在 +
和 -
等默认中缀“相加型”运算符相同的优先级组中。关于 Swift 标准库提供的运算符,以及完整的运算符优先级组和结合性设置,请参考 运算符声明。而更多关于优先级组以及自定义操作符和优先级组的语法,请参考 运算符声明。
注意
当定义前缀与后缀运算符的时候,我们并没有指定优先级。然而,如果对同一个值同时使用前缀与后缀运算符,则后缀运算符会先参与运算。
结果构造器
结果构造器是一种自定义类型,支持添加自然的声明式语法来创建类似列表或者树这样的嵌套数据。使用结果构造器的代码可以包含普通的 Swift 语法,例如用来处理判断条件的 if
,或者处理重复数据的 for
。
下面的代码定义了一些类型用于绘制星星线段和文字线段。
protocolDrawable { funcdraw() ->String } structLine:Drawable { var elements: [Drawable] funcdraw() ->String { return elements.map { $0.draw() }.joined(separator:"") } } structText:Drawable { var content: String init(_content: String) { self.content = content } funcdraw() ->String { return content } } structSpace:Drawable { funcdraw() ->String { return" " } } structStars:Drawable { var length: Int funcdraw() ->String { returnString(repeating:"*", count: length) } } structAllCaps:Drawable { var content: Drawable funcdraw() ->String { return content.draw().uppercased() } }
Drawable
协议定义了绘制所需要遵循的方法,例如线或者形状都需要实现 draw()
方法。Line
结构体用来表示单行线段绘制,给大多数可绘制的元素提供了顶层容器。绘制 Line
时,调用了线段中每个元素的 draw()
,然后将所有结果字符串连成单个字符串。Text
结构体包装了一个字符串作为绘制的一部分。AllCaps
结构体包装另一个可绘制元素,并将元素中所有文本转换为大写。
可以组合这些类型的构造器来创建一个可绘制元素。
let name: String?="Ravi Patel" let manualDrawing =Line(elements: [ Stars(length:3), Text("Hello"), Space(), AllCaps(content: Text((name ??"World") +"!")), Stars(length:2), ]) print(manualDrawing.draw()) // 打印 "***Hello RAVI PATEL!**"
代码没问题,但是不够优雅。AllCaps
后面的括号嵌套太深,可读性不佳。name
为 nil
时使用 “World” 的兜底逻辑必须要依赖 ??
操作符,这在逻辑复杂的时候会更难以阅读。如果还需要 switch
或者 for
循环来构建绘制的一部分,就更难以编写了。使用结果构造器可以将这样的代码重构得更像普通的 Swift 代码。
在类型的定义上加上 @resultBuilder
特性来定义一个结果构造器。比如下面的代码定义了允许使用声明式语法来描述绘制的结果构造器 DrawingBuilder
:
@resultBuilder structDrawingBuilder { staticfuncbuildBlock(_components: Drawable...) -> Drawable { returnLine(elements: components) } staticfuncbuildEither(first: Drawable) -> Drawable { return first } staticfuncbuildEither(second: Drawable) -> Drawable { return second } }
DrawingBuilder
结构体定义了三个方法来实现部分结果构造器语法。buildBlock(_:)
方法添加了在方法块中写多行代码的支持。它将方法块中的多个元素组合成 Line
。buildEither(first:)
和 buildEither(second:)
方法添加了对 if
-else
的支持。
可以在函数形参上应用 @DrawingBuilder
特性,它会将传递给函数的闭包转换为用结果构造器创建的值。例如:
funcdraw(@DrawingBuildercontent: () -> Drawable) -> Drawable { returncontent() } funccaps(@DrawingBuildercontent: () -> Drawable) -> Drawable { returnAllCaps(content: content()) } funcmakeGreeting(forname: String?=nil) -> Drawable { let greeting =draw { Stars(length:3) Text("Hello") Space() caps { iflet name = name { Text(name +"!") } else { Text("World!") } } Stars(length:2) } return greeting } let genericGreeting =makeGreeting() print(genericGreeting.draw()) // 打印 "***Hello WORLD!**" let personalGreeting =makeGreeting(for:"Ravi Patel") print(personalGreeting.draw()) // 打印 "***Hello RAVI PATEL!**"
makeGreeting(for:)
函数将传入的 name
形参用于绘制个性化问候。draw(_:)
和 caps(_:)
函数都传入应用 @DrawingBuilder
特性的单一闭包实参。当调用这些函数时,要使用 DrawingBuilder
定义的特殊语法。Swift 将绘制的声明式描述转换为一系列 DrawingBuilder
的方法调用,构造成最终传递进函数的实参值。例如,Swift 将例子中的 caps(_:)
的调用转换为下面的代码:
let capsDrawing =caps { let partialDrawing: Drawable iflet name = name { let text =Text(name +"!") partialDrawing = DrawingBuilder.buildEither(first: text) } else { let text =Text("World!") partialDrawing = DrawingBuilder.buildEither(second: text) } return partialDrawing }
Swift 将 if-else
方法块转换成调用 buildEither(first:)
和 buildEither(second:)
方法。虽然不会在自己的代码中调用这些方法,但是转换后的结果可以更清晰的理解在使用 DrawingBuilder
语法时 Swift 是如何进行转换的。
为了支持 for
循环来满足某些特殊的绘制语法,需要添加 buildArray(_:)
方法。
extensionDrawingBuilder { staticfuncbuildArray(_components: [Drawable]) -> Drawable { returnLine(elements: components) } } let manyStars =draw { Text("Stars:") for length in1...3 { Space() Stars(length: length) } }
上面的代码中,使用 for
循环创建了一个绘制数组,buildArray(_:)
方法将该数组构建成 Line
。
有关 Swift 如何将构建器语法转换为构建器类型方法的完整信息,查看 结果构造器。