Swift Learning (4) - Advanced Operators (Fully Ver.)

Swift Learning (4) - Advanced Operators (Fully Ver.)

August 12, 2021·Jensen
Jensen

image

除了之前介绍过的基本运算符,Swift还提供了数种可以对数值进行复杂运算的高级运算符。它们包含了在CObjective-C中已经被大家所熟知的位运算符和移位运算符。

自定义结构体、类和枚举时,如果也为它们提供标准Swift运算符的实现,将会非常有用。在Swift中为这些运算符提供自定义的实现非常简单,运算符也会针对不同类型使用对应实现。

开发者不用被预定义的运算符所限制。在Swift中可以自由地定义中缀、前缀、后缀和赋值运算符,它们具有自定义的优先级和关联值。这些运算符在代码中可以像预定义运算符一样使用,开发者甚至可以拓展已有的类型以支持自定义运算符。


位运算符

位运算符可以操作数据结构中每个独立的比特位。它们通常被用在底层开发中,比如图形编程和创建设备驱动。位运算符在处理外部资源的原始数据时也十分有用,比如对自定义通信协议传输的数据进行编码和解码。

Swift支持C语言中的全部位运算符,接下来会一一介绍。

Bitwise NOT Operator(按位取反运算符)

按位取反运算符~对一个数值的全部比特位进行取反。

Bitwise NOT OP

按位取反运算符是一个前缀运算符,直接放在运算数之前,并且运算符与运算数之间不能添加任何空格。

let initialBits: UInt8 = 0b00001111
let invertedBits = ~initialBits  // 等于0b11110000
print("对值\(initialBits)按位取反后的结果是:\(invertedBits)")
---
output: 对值15按位取反后的结果是:240

UInt8类型的整数有8个比特位,可以存储0~255之间的任意整数。这个例子初始化了一个UInt8类型的整数,并赋值位二进制的00001111,它的前4位为0,后4位为1。这个值等价于十进制的15。

接着使用按位取反运算符创建了一个名为invertedBits的常量,这个常量的值域全部位取反后的initialBits相等。即所有的0都变成了1,同时所有的1都变成0。invertedBits的二进制值位11110000,等价于无符号的十进制数240。

Bitwise AND Operator(按位与运算符)

按位与运算符&对两个数的比特位进行合并。它返回一个新的数,只有当两个数的对应位都为1的时候,新数的对应位才为1。

Bitwise AND OP

在下面的示例中,firstSixBitslastSixBits中间4个位的值都为1。使用按位与运算符之后,得到二进制数值00111100,等价与无符号十进制数的60。

let firstSixBits: UInt8 = 0b11111100
let lastSixBits: UInt8 = 0b00111111
let middleFourBits = firstSixBits & lastSixBits  // 等于0b00111100
print("值\(firstSixBits)和值\(lastSixBits)按位与后的结果为:\(middleFourBits)")
---
output: 252和值63按位与后的结果为:60

Bitwise OR Operator(按位或运算符)

按位或运算符/可以对两个数的比特位进行比较。它返回一个新的数,只要两个数的对应位中有任意一个为1时,新数的对应位就为1。

Bitwise OR OP

在下面的示例中,someBitsmoreBits存在不同的位被设置为1。使用按位或运算符之后,得到二进制数值11111110,等价于无符号十进制的254。

let someBits: UInt8 = 0b10110010
let moreBits: UInt8 = 0b01011110
let combinedBits = someBits | moreBits
print("值\(someBits)和值\(moreBits)按位或后的结果为:\(combinedBits)")
---
output: 178和值94按位或后的结果为:254

Bitwise XOR Operator(按位异或运算符)

按位异或运算符,或称“排外的或运算符”^,可以对两个数的比特位进行比较。它返回一个新的数,当两个数的对应位不相同时,新数的对应位就为1,并且对应位相同时则为0。

Bitwise XOR OP

在下面的示例当中,firstBitsotherBits都有一个自己为1而对方为0的位。按位异或运算符将新数的这两个位设置为1。在其余的位上firstBitsotherBits是相同的,所以设置为0。

let firstBits: UInt8 = 0b00010100
let otherBits: UInt8 = 0b00000101
let outputBits = firstBits ^ otherBits
print("值\(firstBits)和值\(otherBits)按位异或后的结果为:\(outputBits)")
---
output: 20和值5按位异或后的结果为:17

Bitwise Left and Right Shift Operators(按位左移、右移运算符)

按位左移运算符<<和按位右移运算符>>可以对一个数的所有位进行指定位数的左移和右移,但是需要遵守下面定义的规则。

对一个数进行按位左移或者按位右移,相当于对这个数进行乘以2或者除以2的运算。将一个整数左移一位,等价于将这个数乘以2,同样地,将一个整数右移一位,等价于将这个数除以2。

无符号整数的的移位运算

对无符号整数进行移位的规则如下:

1. 已存在的位按指定的位数进行左移和右移

2. 任何因移动而超出整型存储范围的位都会被丢弃

3. 用0来填充移位后产生的空白位

上述方法被称为逻辑移位

Bit shift unsigned

下面的代码演示了Swift中的移位运算。

let shiftBits: UInt8 = 0b00000100
print("\(shiftBits)向左移1位:\(shiftBits << 1)")  // 0b00001000
print("\(shiftBits)向左移2位:\(shiftBits << 2)")  // 0b00010000
print("\(shiftBits)向左移5位:\(shiftBits << 5)")  // 0b10000000
print("\(shiftBits)向左移6位:\(shiftBits << 6)")  // 0b00000000
print("\(shiftBits)向右移2位:\(shiftBits >> 2)")  // 0b00000001
---
output: 4向左移1位:8
4向左移2位:16
4向左移5位:128
4向左移6位:0
4向右移2位:1

可以使用移位运算对其他的数据类型进行编码和解码。

let pink: UInt32 = 0xCC6699; print(pink)
let redComponent  = (pink & 0xFF0000) >> 16  // redComponent是0xCC,即204
let greenComponent = (pink & 0x00FF00) >> 8  // greenComponent是0x66,即102
let blueComponent = (pink & 0x0000FF) // blueComponent是0x99,即153
print("pink中红色分量为:\(redComponent)")
print("pink中绿色分量为:\(greenComponent)")
print("pink中蓝色分量为:\(blueComponent)")
---
output: 13395609
pink中红色分量为:204
pink中绿色分量为:102
pink中蓝色分量为:153

在上述示例中,使用了一个命名为pinkUInt32型常量来存储Cascading Style Sheets (CSS)中粉色的颜色值。该CSS的颜色值#CC6699,在Swift中表示为十六进制的0xCC6699。然后利用按位与运算符&和按位右移运算符>>从这个颜色中分解出红(CC)、绿(66)蓝(99)三个部分。

红色部分(redComponent)是通过对0xCC66990xFF0000进行按位与运算后得到的。0xFF0000中的0部分“掩盖”了0xCC6699中的第二第三个字节,只留下0xCC0000。然后将这个数向右移16位后就变成0x0000CC,也就是十进制数值的204。

有符号整数的移位运算

对比无符号整数,有符号整数的移位运算相对复杂得多,这种复杂性源于有符号整数的二进制表现形式。(为了简单起见,以下的示例都是基于8比特的有符号整数,但是其中的原理对于任何位数的有符号整数都是通用的。)

有符号整数使用第一个比特位(通常被称为符号位)来表示这个数的正负。符号位为0代表正数,符号位为1代表负数。

其余的比特位(通常被称为数值位)存储了实际的值。有符号正整数和无符号数的存储方式都是一样的,都是从0开始算起。这是值为 4 的 Int8 型整数的二进制位表现形式:

Bit shift signed four

符号位为0(代表这是一个“正数”),另外7位则代表了十进制数值4的二进制表示。

负数的存储方式略有不同,它存储2的n次方减去实际值的绝对值,这里的n是数值位的位数。一个8比特位的数有七个比特位是数值位,所以是2的7次方,即128。这是值为 -4 的 Int8 型整数的二进制表现形式:

Bit shift signed minus four

这次的符号位为1,说明这是一个负数,另外7个位则代表了数值124(即2^7 - |-4| = 124)的二进制表示。

负数的表示通常被称为二进制补码。用这种方式来表示负数乍一看有点奇怪,但是它却有几个优点。

首先,如果想对-1和-4进行加法运算,只需要对这两个数的全部8个比特位执行标准的二进制相加(包括符号位),并且将计算结果中超出8位的数值丢弃。

其次,使用二进制补码可以使负数的按位左移和右移运算得到跟正数同样地效果。即每向左移一位就可以将自身的数值乘以2,每向右移一位就可以将自身的数值除以2。要达到此目的,对有符号整数的右移有一个额外的规则:当对有符号整数进行按位右移运算时,遵循无符号整数相同的规则,但是对于移位产生的空白位使用符号位进行填充,而不是用0。

这个行为可以确保有符号整数的符号位不会因为右移运算而改变,这通常被称为算术移位


溢出运算符

当像一个整数类型的常量或者变量赋予超过它容量的值时,Swift默认会报错,而不是允许生成一个无效的数。这个行为为开发者在运算过大或者过小的数时提供了额外的安全性。

例如,Int16型整数能够容纳的有符号整数的范围是-32768~32767。当为一个Int16类型的变量或常量赋予超过这个范围的值时,系统就会报错。

var potentialOverFlow = Int16.max
//potentialOverFlow += 1  // 若取消这行注释,运行后会报错:Swift runtime failure: arithmetic overflow

在赋值时为过大或过小的情况提供错误处理,能让开发者在处理边界值时更加灵活。

然而,当开发者希望的时候也可以选择让系统在数值溢出的时候采取截断处理,而非报错。Swift提供的三个溢出运算符来让系统支持整数溢出运算。这些运算符都是以&开头的:

  • 溢出加法&+
  • 溢出减法&-
  • 溢出乘法&*

数值溢出

数值有可能出现上溢或者下溢。

下面的例子演示了当对一个无符号整数使用溢出加法进行上溢运算时会发生什么。

var unsignedOverFlow = UInt8.max  // unsignedOverFlow等于UInt8所能容纳的最大整数255
unsignedOverFlow = unsignedOverFlow &+ 1  // 此时unsignedOverFlow等于0
print("UInt8上溢后的值:\(unsignedOverFlow)")
---
output: UInt8上溢后的值:0

unsignedOverFlow被初始化为UInt8所能容纳的最大整数(255,以二进制表示即11111111),然后使用溢出加法运算符&+对其进行加1运算。使得它的二进制表示正好超出UInt8所能容纳的位数,也就导致了数值的溢出。数值溢出后,仍然留在UInt8边界内的值是00000000,也就是十进制数值0。

Over flow addition

当允许对一个无符号整数进行下溢运算时也会产生类似的情况。这里有一个使用溢出减法运算符&-的例子。

unsignedOverFlow = UInt8.min  // unsignedOverFlow等于UInt8所能容纳的最小整数0
unsignedOverFlow = unsignedOverFlow &- 1  // 此时unsignedOverFlow等于255
print("UInt8下溢后的值:\(unsignedOverFlow)")
---
output: UInt8下溢后的值:255

UInt8型整数能够容纳的最小值是0,以二进制表示即为00000000。当使用溢出减法运算符对其进行减1运算时,数值会产生下溢并被截断为11111111,也就是十进制数值的255。

Over flow unsigned subtraction

溢出也会发生在有符号整型上。针对有符号整型的所有溢出加法或者减法运算都是按位运算的方式执行的。符号位也要参与计算,正如按位左移、右移运算符所描述的那样。

var signedOverFlow = Int8.min  // signedOverFlow等于Int8所能容纳的最小整数-128
signedOverFlow = signedOverFlow &- 1  // 此时signedOverFlow等于127
print("Int8下溢后的值:\(signedOverFlow)")
---
output: Int8下溢后的值:127

Int8型整数能容纳的最小值是-128,以二进制形式表示即10000000。当使用溢出减法运算符对其进行减1运算时,符号位被翻转,得到二进制数值01111111,也就是十进制数值的127,这个值也是Int8型整数所能容纳的最大值。

Over flow signed subtraction

对于无符号和有符号整型数值来说,当出现上溢时,它们会从数值所能容纳的最大数变成最小数。同样地,当发生下溢时,它们会从数值的最小数变成数值的最大数。


优先级和结合性

运算符的优先级使得一些运算符优先于其他运算符,也就意味着优先级更高的运算符会先被执行。

结合性定义了相同优先级的运算符是如何结合的,也就是说,是与左边的结合为一组还是与右边的结合为一组。

当考虑一个复合表达式的计算顺序时,运算符的优先级和结合性是非常重要的。举例来说,运算符优先级解释了为什么下面这个表达式的运算结果会是17。

var result = 2 + 3 % 4 * 5  // result的值是17
print("2 + 3 % 4 * 5的值是:\(result)")
---
output: 2 + 3 % 4 * 5的值是:17

如果直接从左到右进行运算,可能会认为计算的过程是这样的:

  • 2 + 3 = 5
  • 5 % 4 = 1
  • 1 * 5 = 5

但是正确答案是17而不是5。优先级高的运算符要先于优先级低的运算符进行计算。与C语言类似,在Swift中,乘法运算符*与取余运算符%的优先级要高于加法运算符+。因此,它们的计算顺序也要先于加法运算。

而乘法运算与取余运算的优先级相同,这时为了得到正确的运算顺序,还需要考虑结合性。乘法运算与取余运算都是左结合的。可以将这考虑成,从它们的左边开始为这两部分表达式都隐式地加上括号。即 2 + 3 % 4 * 5 ==> 2 + ((3 % 4) * 5) ==> 2 + (3 * 5) ==> 2 + 15 ==> 17。因此计算结果为17。


运算符函数

类与结构体可以为现有的运算符提供自定义的实现。这通常被称为运算符的重载。

下面的例子展示了如何让自定义的结构体支持加法运算符+。算术加法运算符是一个二元运算符,因其是对两个值进行运算,同时它还可以被称为中缀运算符,因为它出现在两个值中间。

例子中定义了一个名为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类型的返回值。

在这个实现中,输入参数分别被命名为leftright,代表在+运算符左边和右边的两个Vector2D实例,这个实例的xy分别等于作为参数的两个实例的xy的值之和。

这个类方法可以在任意两个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)
print("两个Vector2D类型的向量相加后的值为:\(combinedVector)")
---
output: 两个Vector2D类型的向量相加后的值为:Vector2D(x: 5.0, y: 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来说,该运算将其xy属性的正负性都进行了改变。

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实例
print("-positive的值为:\(negative)")
print("-negative的值为:\(alsoPositive)")
---
output: -positive的值为:Vector2D(x: -3.0, y: -4.0)
-negative的值为:Vector2D(x: 3.0, y: 4.0)

复合赋值运算符

复合赋值运算符将赋值运算符=与其它运算符进行结合。例如,将加法与赋值结合成加法赋值运算符+=。在实现的时候,需要把运算符的左参数设置成inout类型,因为这个参数的值会在运算符函数内直接被修改。

下面的例子中,对Vector2D实例实现了一个加法赋值运算符函数。

extension Vector2D {
    static func += (left: inout Vector2D, right: Vector2D) {
        left = left + right  // 因为Vector2D的加法运算在之前已经定义过了,所以在这里无需再次定义,直接利用现有的加法运算符函数
    }
}

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)
print("Vector2D类型的加法赋值运算之后的值为:\(original)")
---
output: Vector2D类型的加法赋值运算之后的值为:Vector2D(x: 4.0, y: 6.0)

注意

不能对默认的赋值运算符=进行重载。只有复合赋值运算可以被重载。同样地,也无法对三元条件运算符(a ? b : c)进行重载。

等价运算符

通常情况下,自定义的类和结构体没有对等价运算符进行默认实现,等价运算符通常被称为相等运算符==和不等运算符!=

为了使用等价运算符对自定义的类型进行判等运算,需要为“相等”运算符提供自定义实现,实现的方法与其他中缀运算符一样,并且增加对标准库Equatable协议的遵循。

extension Vector2D: Equatable {  // 遵循Equatable协议便可以包含对“相等”运算符(==)和“不等”运算符(!=)的实现
    static func == (left: Vector2D, right: Vector2D) -> Bool {
        return (left.x == right.x) && (left.y == right.y)
    }
}

上述代码实现了“相等”运算符(==)来判断两个Vector2D实例是否相等。对于Vector2D来说,“相等”意味着“两个实例的x和y都相等”,这也是代码中用来进行判等的逻辑。如果已经实现了“相等”运算符,通常情况下不需要再去实现“不等”运算符(!=)。标准库对于“不等”运算符提供了默认的实现,它简单地讲“相等”运算符的结果进行取反后返回。

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!")
}
let threeTwo = Vector2D(x: 3.0, y: 2.0)
if twoThree != threeTwo {  // 已经实现了“相等”运算符,故不需要再去实现“不等”运算符
    print("These two vectors are not equivalent!")
}
---
output: These two vectors are equivalent!
These two vectors are not equivalent!

多数简单情况下,可以让Swift合成等价运算符的实现(遵循Equatable协议等)。


自定义运算符

除了实现标准运算符,在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和afterDoubling现在的值均为(2.0, 8.0)
print("toBeDoubled的值为:\(toBeDoubled)")
print("afterDoubling的值为:\(afterDoubling)")
---
output: toBeDoubled的值为:Vector2D(x: 2.0, y: 8.0)
afterDoubling的值为:Vector2D(x: 2.0, y: 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)
print("plusMinusVector的值为:\(plusMinusVector)")
---
output: plusMinusVector的值为:Vector2D(x: 4.0, y: -2.0)

这个运算符把两个向量的x值相加,同时从第一个向量的y值中减去第二个向量的y。因为它本质上是属于“相加型”运算符,所以将它放置在+-等默认中缀“相加型”运算符相同的优先级组(AdditionPrecedence)中。

注意

当定义前缀和后缀运算符的时候,并没有指定优先级。然而,如果对一个值同时使用前缀和后缀运算符,则后缀运算符会先参与运算。


结果构造器

结果构造器是一种自定义类型,支持添加自然的声明式语法来创建类似列表或者树这样的嵌套数据。使用结果构造器代码可以包含普通的Swift语法,例如用来处理判断条件的if,或者处理重复数据的for

下面的代码定义了一些类型用于绘制星星线段和文字线段。

protocol Drawable {
    func draw() -> String
}
struct Line: Drawable {
    var elements: [Drawable]
    func draw() -> String {
        return elements.map { $0.draw() }.joined(separator: "")  // 绘制Line时,调用了线段中每个元素的draw(),然后将所有结果字符串连城单个字符串
    }
}
struct Text: Drawable {
    var content: String
    init(_ content: String) { self.content = content }
    func draw() -> String {
        return content
    }
}
struct Space: Drawable {
    func draw() -> String {
        return " "
    }
}
struct Stars: Drawable {
    var length: Int
    func draw() -> String {
        return String(repeating: "*", count: length)
    }
}
struct AllCaps: Drawable {
    var content: Drawable
    func draw() -> String {
        return content.draw().uppercased()
    }
}

Drawable协议定制了绘制所需要遵循的方法,例如线条或者形状都需要实现draw()方法。Line结构体用来表示单行线段绘制,给大多数可绘制的元素提供了顶层容器。绘制Line时,调用了线段中每个元素的draw(),然后将所有结果字符串连城单个字符串。Text结构体包装了一个字符串作为绘制的一部分。AllCaps结构体包装另一个可绘制元素,并将元素中所有文本转换成大写。

可以组合这些类型的构造器来创建一个可绘制元素。

let name: String? = "Jensen Jon"
let manualDrawing = Line(elements: [Stars(length: 3), Text("Hello"), Space(), AllCaps(content: Text((name ?? "World") + "!")), Stars(length: 2)])
print(manualDrawing.draw())  // 打印“***Hello JENSEN JON!**”
---
output: ***Hello JENSEN JON!**

代码没问题,但是不够优雅。AllCaps后面的括号嵌套太深,可读性不佳。namenil时使用“World”的兜底逻辑必须要依赖??操作符,这在逻辑复杂的时候会更加难以阅读。如果还需要switch或者for循环来构建绘制的一部分,就更难以编写了。使用结果构造器可以将这样的代码重构得更像普通的Swift代码。

在类型的定义上加上@resultBuilder特性来定义一个结果构造器。比如下面的代码定义了允许使用声明式语法来描述绘制的结果构造器DrawingBuilder

@resultBuilder
struct DrawingBuilder {
    static func buildBlock(_ components: Drawable...) -> Drawable {
        return Line(elements: components)
    }
    static func buildEither(first component: Drawable) -> Drawable {
        return component
    }
    static func buildEither(second component: Drawable) -> Drawable {
        return component
    }
}

DrawingBuilder结构体定义了三个方法来实现部分结果构造器语法。

buildBlock(_:)方法添加了在方法块中写多行代码的支持。它将方法块中的多个元素组合成LinebuildEither(first:)方法和buildEither(second:)方法添加了对if-else的支持。

可以在函数形参上应用@DrawingBuilder特性,它会将传递给函数的闭包转换为用结果构造器创建的值。

func draw(@DrawingBuilder content: () -> Drawable) -> Drawable {
    return content()
}
func caps(@DrawingBuilder content: () -> Drawable) -> Drawable {
    return AllCaps(content: content())
}

func makeGreeting(for name: String? = nil) -> Drawable {
    let greeting = draw {
        Stars(length: 3)
        Text("Hello")
        Space()
        caps {
            if let name = name {
                Text(name + "!")
            } else {
                Text("World!")
            }
        }
        Stars(length: 2)
    }
    return greeting
}

let genericGreeting = makeGreeting()
print(genericGreeting.draw())  // 打印“***Hello WORLD!**”

let personalGreeting = makeGreeting(for: "Jensen Jon")
print(personalGreeting.draw())  // 打印“***Hello JENSEN JON!**”
---
output: ***Hello WORLD!**
***Hello JENSEN JON!**

makeGreeting(for:)函数将传入的name形参用于绘制个性化问候。

draw(_:)caps(_:)函数都传入应用@DrawingBuilder特性的单一闭包实参。当调用这些函数时,要使用DrawingBuilder定义的特殊语法。Swift将绘制的声明式描述转换成一系列DrawingBuilder方法调用,构造成最终传递进函数的实参值。例如,Swift将例子中的caps(_:)的调用转换为下面的代码。

let capsDrawing = caps {
    let partialDrawing: Drawable
    if let name = name {
        let text = Text(name + "!")
        partialDrawing = DrawingBuilder.buildEither(first: text)
    } else {
        let text = Text("World!")
        partialDrawing = DrawingBuilder.buildEither(second: text)
    }
    return partialDrawing
}

Swiftif-else方法块转换成调用buildEither(first:)buildEither(second:)方法,虽然不会在自己的代码中调用这些方法,但是转换后的结果可以更清晰的理解在使用DrawingBuilder语法时Swift是如何进行转换的。

为了支持for循环来满足某些特殊的绘制语法,需要添加buildArray(:_)方法。

extension DrawingBuilder {
    static func buildArray(_ components: [Drawable]) -> Drawable {
        return Line(elements: components)
    }
}
func makeStars() -> Drawable {
    let manyStars = draw {
        Text("Stars:")
        for length in 1...3 {
            Space()
            Stars(length: length)  // for循环中自动调用buildArray(_:)方法
        }
    }
    return manyStars
}
let generateStars = makeStars()
print(generateStars.draw())
---
output: Stars: * ** ***

上面的代码中,使用for循环创建了一个绘制数组,buildArray(_:)方法将该数组构建成Line

Last updated on