swift高级运算符

分類 编程语言, swift

高级运算符

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

与 C 语言中的算术运算符不同,Swift 中的算术运算符默认是不会溢出的。所有溢出行为都会被捕获并报告为错误。如果想让系统允许溢出行为,可以选择使用 Swift 中另一套默认支持溢出的运算符,比如溢出加法运算符(&+)。所有的这些溢出运算符都是以 & 开头的。

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

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

位运算符

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

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

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

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

Art/bitwiseNOT_2x.png

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

1
2
let initialBits: UInt8 = 0b00001111
let invertedBits = ~initialBits // 等于 0b11110000

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

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

Bitwise AND Operator(按位与运算符)

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

Art/bitwiseAND_2x.png

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

1
2
3
let firstSixBits: UInt8 = 0b11111100
let lastSixBits: UInt8 = 0b00111111
let middleFourBits = firstSixBits & lastSixBits // 等于 00111100

Bitwise OR Operator(按位或运算符)

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

Art/bitwiseOR_2x.png

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

1
2
3
let someBits: UInt8 = 0b10110010
let moreBits: UInt8 = 0b01011110
let combinedbits = someBits | moreBits // 等于 11111110

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

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

Art/bitwiseXOR_2x.png

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

1
2
3
let firstBits: UInt8 = 0b00010100
let otherBits: UInt8 = 0b00000101
let outputBits = firstBits ^ otherBits // 等于 00010001

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

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

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

无符号整数的移位运算

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

  1. 已存在的位按指定的位数进行左移和右移。
  2. 任何因移动而超出整型存储范围的位都会被丢弃。
  3. 0 来填充移位后产生的空白位。

这种方法称为逻辑移位

以下这张图展示了 11111111 << 1(即把 11111111 向左移动 1 位),和 11111111 >> 1(即把 11111111 向右移动 1 位)的结果。蓝色的数字是被移位的,灰色的数字是被抛弃的,橙色的 0 则是被填充进来的:

Art/bitshiftUnsigned_2x.png

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

1
2
3
4
5
6
let shiftBits: UInt8 = 4 // 即二进制的 00000100
shiftBits << 1 // 00001000
shiftBits << 2 // 00010000
shiftBits << 5 // 10000000
shiftBits << 6 // 00000000
shiftBits >> 2 // 00000001

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

1
2
3
4
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

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

红色部分是通过对 0xCC66990xFF0000 进行按位与运算后得到的。0xFF0000 中的 0 部分“掩盖”了 OxCC6699 中的第二、第三个字节,使得数值中的 6699 被忽略,只留下 0xCC0000

然后,将这个数向右移动 16 位(>> 16)。十六进制中每两个字符占用 8 个比特位,所以移动 16 位后 0xCC0000 就变为 0x0000CC。这个数和 0xCC 是等同的,也就是十进制数值的 204

同样的,绿色部分通过对 0xCC66990x00FF00 进行按位与运算得到 0x006600。然后将这个数向右移动 8 位,得到 0x66,也就是十进制数值的 102

最后,蓝色部分通过对 0xCC66990x0000FF 进行按位与运算得到 0x000099。这里不需要再向右移位,而 0x000099 也就是 0x99 ,也就是十进制数值的 153

有符号整数的移位运算

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

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

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

Art/bitshiftSignedFour_2x.png

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

负数的存储方式略有不同。它存储 2n 次方减去其实际值的绝对值,这里的 n 是数值位的位数。一个 8 比特位的数有 7 个比特位是数值位,所以是 27 次方,即 128

这是值为 -4Int8 型整数的二进制表现形式:

Art/bitshiftSignedMinusFour_2x.png

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

Art/bitshiftSignedMinusFourValue_2x.png

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

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

Art/bitshiftSignedAddition_2x.png

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

Art/bitshiftSigned_2x.png

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

由于正数和负数的特殊存储方式,在对它们进行右移的时候,会使它们越来越接近 0。在移位的过程中保持符号位不变,意味着负整数在接近 0 的过程中会一直保持为负。

溢出运算符

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

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

1
2
3
4
var potentialOverflow = Int16.max
// potentialOverflow 的值是 32767,这是 Int16 能容纳的最大整数
potentialOverflow += 1
// 这里会报错

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

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

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

数值溢出

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

这个示例演示了当我们对一个无符号整数使用溢出加法(&+)进行上溢运算时会发生什么:

1
2
3
4
var unsignedOverflow = UInt8.max
// unsignedOverflow 等于 UInt8 所能容纳的最大整数 255
unsignedOverflow = unsignedOverflow &+ 1
// 此时 unsignedOverflow 等于 0

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

Art/overflowAddition_2x.png

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

1
2
3
4
var unsignedOverflow = UInt8.min
// unsignedOverflow 等于 UInt8 所能容纳的最小整数 0
unsignedOverflow = unsignedOverflow &- 1
// 此时 unsignedOverflow 等于 255

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

Art/overflowUnsignedSubtraction_2x.png

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

1
2
3
4
var signedOverflow = Int8.min
// signedOverflow 等于 Int8 所能容纳的最小整数 -128
signedOverflow = signedOverflow &- 1
// 此时 signedOverflow 等于 127

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

Art/overflowSignedSubtraction_2x.png

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

优先级和结合性

运算符的优先级使得一些运算符优先于其他运算符;它们会先被执行。

结合性定义了相同优先级的运算符是如何结合的,也就是说,是与左边结合为一组,还是与右边结合为一组。可以将其理解为“它们是与左边的表达式结合的”,或者“它们是与右边的表达式结合的”。

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

1
2
2 + 3 % 4 * 5
// 结果是 17

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

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

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

而乘法运算与取余运算的优先级相同。这时为了得到正确的运算顺序,还需要考虑结合性。乘法运算与取余运算都是左结合的。可以将这考虑成,从它们的左边开始为这两部分表达式都隐式地加上括号:

1
2 + ((3 % 4) * 5)

(3 % 4) 等于 3,所以表达式相当于:

1
2 + (3 * 5)

3 * 5 等于 15,所以表达式相当于:

1
2 + 15

因此计算结果为 17

有关 Swift 标准库提供的操作符信息,包括操作符优先级组和结核性设置的完整列表,请参见 操作符声明

注意

相对 C 语言和 Objective-C 来说,Swift 的运算符优先级和结合性规则更加简洁和可预测。但是,这也意味着它们相较于 C 语言及其衍生语言并不是完全一致。在对现有的代码进行移植的时候,要注意确保运算符的行为仍然符合你的预期。

运算符函数

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

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

例子中定义了一个名为 Vector2D 的结构体用来表示二维坐标向量 (x, y),紧接着定义了一个可以将两个 Vector2D 结构体实例进行相加的运算符函数

1
2
3
4
5
6
7
8
9
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 实例。函数返回了一个新的 Vector2D 实例,这个实例的 xy 分别等于作为参数的两个实例的 xy 的值之和。

这个类方法可以在任意两个 Vector2D 实例中间作为中缀运算符来使用:

1
2
3
4
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)。这个过程如下图示:

Art/vectorAddition_2x.png

前缀和后缀运算符

上个例子演示了一个二元中缀运算符的自定义实现。类与结构体也能提供标准一元运算符的实现。一元运算符只运算一个值。当运算符出现在值之前时,它就是前缀的(例如 -a),而当它出现在值之后时,它就是后缀的(例如 b!)。

要实现前缀或者后缀运算符,需要在声明运算符函数的时候在 func 关键字之前指定 prefix 或者 postfix 修饰符:

1
2
3
4
5
extension Vector2D {
static prefix func - (vector: Vector2D) -> Vector2D {
return Vector2D(x: -vector.x, y: -vector.y)
}
}

这段代码为 Vector2D 类型实现了一元运算符(-a)。由于该运算符是前缀运算符,所以这个函数需要加上 prefix 修饰符。

对于简单数值,一元负号运算符可以对它们的正负性进行改变。对于 Vector2D 来说,该运算将其 xy 属性的正负性都进行了改变:

1
2
3
4
5
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 实例实现了一个加法赋值运算符函数:

1
2
3
4
5
extension Vector2D {
static func += (left: inout Vector2D, right: Vector2D) {
left = left + right
}
}

因为加法运算在之前已经定义过了,所以在这里无需重新定义。在这里可以直接利用现有的加法运算符函数,用它来对左值和右值进行相加,并再次赋值给左值:

1
2
3
4
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 协议的遵循:

1
2
3
4
5
extension Vector2D: Equatable {
static func == (left: Vector2D, right: Vector2D) -> Bool {
return (left.x == right.x) && (left.y == right.y)
}
}

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

现在我们可以使用这两个运算符来判断两个 Vector2D 实例是否相等:

1
2
3
4
5
6
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 为以下数种自定义类型提供等价运算符的默认实现:

  • 只拥有存储属性,并且它们全都遵循 Equatable 协议的结构体
  • 只拥有关联类型,并且它们全都遵循 Equatable 协议的枚举
  • 没有关联类型的枚举

在类型原始的声明中声明遵循 Equatable 来接收这些默认实现。

下面为三维位置向量 (x, y, z) 定义的 Vector3D 结构体,与 Vector2D 类似。由于 xyz 属性都是 Equatable 类型,Vector3D 获得了默认的等价运算符实现。

1
2
3
4
5
6
7
8
9
10
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.")
}
// 打印“These two vectors are also equivalent.”

自定义运算符

除了实现标准运算符,在 Swift 中还可以声明和实现自定义运算符。可以用来自定义运算符的字符列表请参考 运算符

新的运算符要使用 operator 关键字在全局作用域内进行定义,同时还要指定 prefixinfix 或者 postfix 修饰符:

1
prefix operator +++

上面的代码定义了一个新的名为 +++ 的前缀运算符。对于这个运算符,在 Swift 中并没有已知的意义,因此在针对 Vector2D 实例的特定上下文中,给予了它自定义的意义。对这个示例来讲,+++ 被实现为“前缀双自增”运算符。它使用了前面定义的复合加法运算符来让矩阵与自身进行相加,从而让 Vector2D 实例的 x 属性和 y 属性值翻倍。你可以像下面这样通过对 Vector2D 添加一个 +++ 类方法,来实现 +++ 运算符:

1
2
3
4
5
6
7
8
9
10
11
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 优先组:

1
2
3
4
5
6
7
8
9
10
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访问控制

分類 编程语言, swift

访问控制

访问控制可以限定其它源文件或模块中的代码对你的代码的访问级别。这个特性可以让我们隐藏代码的一些实现细节,并且可以为其他人可以访问和使用的代码提供接口。

你可以明确地给单个类型(类、结构体、枚举)设置访问级别,也可以给这些类型的属性、方法、构造器、下标等设置访问级别。协议也可以被限定在一定的范围内使用,包括协议里的全局常量、变量和函数。

Swift 不仅提供了多种不同的访问级别,还为某些典型场景提供了默认的访问级别,这样就不需要我们在每段代码中都申明显式访问级别。其实,如果只是开发一个单一 target 的应用程序,我们完全可以不用显式声明代码的访问级别。

注意

为了简单起见,对于代码中可以设置访问级别的特性(属性、基本类型、函数等),在下面的章节中我们会称之为“实体”。

模块和源文件

Swift 中的访问控制模型基于模块和源文件这两个概念。

模块指的是独立的代码单元,框架或应用程序会作为一个独立的模块来构建和发布。在 Swift 中,一个模块可以使用 import 关键字导入另外一个模块。

在 Swift 中,Xcode 的每个 target(例如框架或应用程序)都被当作独立的模块处理。如果你是为了实现某个通用的功能,或者是为了封装一些常用方法而将代码打包成独立的框架,这个框架就是 Swift 中的一个模块。当它被导入到某个应用程序或者其他框架时,框架内容都将属于这个独立的模块。

源文件就是 Swift 中的源代码文件,它通常属于一个模块,即一个应用程序或者框架。尽管我们一般会将不同的类型分别定义在不同的源文件中,但是同一个源文件也可以包含多个类型、函数之类的定义。

访问级别

Swift 为代码中的实体提供了五种不同的访问级别。这些访问级别不仅与源文件中定义的实体相关,同时也与源文件所属的模块相关。

  • OpenPublic 级别可以让实体被同一模块源文件中的所有实体访问,在模块外也可以通过导入该模块来访问源文件里的所有实体。通常情况下,你会使用 Open 或 Public 级别来指定框架的外部接口。Open 和 Public 的区别在后面会提到。
  • Internal 级别让实体被同一模块源文件中的任何实体访问,但是不能被模块外的实体访问。通常情况下,如果某个接口只在应用程序或框架内部使用,就可以将其设置为 Internal 级别。
  • File-private 限制实体只能在其定义的文件内部访问。如果功能的部分细节只需要在文件内使用时,可以使用 File-private 来将其隐藏。
  • Private 限制实体只能在其定义的作用域,以及同一文件内的 extension 访问。如果功能的部分细节只需要在当前作用域内使用时,可以使用 Private 来将其隐藏。

Open 为最高访问级别(限制最少),Private 为最低访问级别(限制最多)。

Open 只能作用于类和类的成员,它和 Public 的区别如下:

  • Public 或者其它更严访问级别的类,只能在其定义的模块内部被继承。
  • Public 或者其它更严访问级别的类成员,只能在其定义的模块内部的子类中重写。
  • Open 的类,可以在其定义的模块中被继承,也可以在引用它的模块中被继承。
  • Open 的类成员,可以在其定义的模块中子类中重写,也可以在引用它的模块中的子类重写。

把一个类标记为 open,明确的表示你已经充分考虑过外部模块使用此类作为父类的影响,并且设计好了你的类的代码了。

访问级别基本原则

Swift 中的访问级别遵循一个基本原则:实体不能定义在具有更低访问级别(更严格)的实体中

例如:

  • 一个 Public 的变量,其类型的访问级别不能是 Internal,File-private 或是 Private。因为无法保证变量的类型在使用变量的地方也具有访问权限。
  • 函数的访问级别不能高于它的参数类型和返回类型的访问级别。因为这样就会出现函数可以在任何地方被访问,但是它的参数类型和返回类型却不可以的情况。

关于此原则在各种情况下的具体表现,将在下文有所体现。

默认访问级别

如果你没有为代码中的实体显式指定访问级别,那么它们默认为 internal 级别(有一些例外情况,稍后会进行说明)。因此,在大多数情况下,我们不需要显式指定实体的访问级别。

单 target 应用程序的访问级别

当你编写一个单目标应用程序时,应用的所有功能都是为该应用服务,而不需要提供给其他应用或者模块使用,所以我们不需要明确设置访问级别,使用默认的访问级别 Internal 即可。但是,你也可以使用 fileprivate 访问或 private 访问级别,用于隐藏一些功能的实现细节。

框架的访问级别

当你开发框架时,就需要把一些对外的接口定义为 Open 或 Public,以便使用者导入该框架后可以正常使用其功能。这些被你定义为对外的接口,就是这个框架的 API。

注意

框架的内部实现仍然可以使用默认的访问级别 internal,当你需要对框架内部其它部分隐藏细节时可以使用 privatefileprivate。对于框架的对外 API 部分,你就需要将它们设置为 openpublic 了。

单元测试 target 的访问级别

当你的应用程序包含单元测试 target 时,为了测试,测试模块需要访问应用程序模块中的代码。默认情况下只有 openpublic 级别的实体才可以被其他模块访问。然而,如果在导入应用程序模块的语句前使用 @testable 特性,然后在允许测试的编译设置(Build Options -> Enable Testability)下编译这个应用程序模块,单元测试目标就可以访问应用程序模块中所有内部级别的实体。

访问控制语法

通过修饰符 openpublicinternalfileprivateprivate 来声明实体的访问级别:

1
2
3
4
5
6
7
8
9
public class SomePublicClass {}
internal class SomeInternalClass {}
fileprivate class SomeFilePrivateClass {}
private class SomePrivateClass {}

public var somePublicVariable = 0
internal let someInternalConstant = 0
fileprivate func someFilePrivateFunction() {}
private func somePrivateFunction() {}

除非专门指定,否则实体默认的访问级别为 internal,可以查阅 默认访问级别 这一节。这意味着在不使用修饰符显式声明访问级别的情况下,SomeInternalClasssomeInternalConstant 仍然拥有隐式的 internal

1
2
class SomeInternalClass {}   // 隐式 internal
var someInternalConstant = 0 // 隐式 internal

自定义类型

如果想为一个自定义类型指定访问级别,在定义类型时进行指定即可。新类型只能在它的访问级别限制范围内使用。例如,你定义了一个 fileprivate 级别的类,那这个类就只能在定义它的源文件中使用,可以作为属性类型、函数参数类型或者返回类型,等等。

一个类型的访问级别也会影响到类型成员(属性、方法、构造器、下标)的默认访问级别。如果你将类型指定为 private 或者 fileprivate 级别,那么该类型的所有成员的默认访问级别也会变成 private 或者 fileprivate 级别。如果你将类型指定为 internalpublic(或者不明确指定访问级别,而使用默认的 internal ),那么该类型的所有成员的默认访问级别将是 internal

重点

上面提到,一个 public 类型的所有成员的访问级别默认为 internal 级别,而不是 public 级别。如果你想将某个成员指定为 public 级别,那么你必须显式指定。这样做的好处是,在你定义公共接口的时候,可以明确地选择哪些接口是需要公开的,哪些是内部使用的,避免不小心将内部使用的接口公开。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public class SomePublicClass {                  // 显式 public 类
public var somePublicProperty = 0 // 显式 public 类成员
var someInternalProperty = 0 // 隐式 internal 类成员
fileprivate func someFilePrivateMethod() {} // 显式 fileprivate 类成员
private func somePrivateMethod() {} // 显式 private 类成员
}

class SomeInternalClass { // 隐式 internal 类
var someInternalProperty = 0 // 隐式 internal 类成员
fileprivate func someFilePrivateMethod() {} // 显式 fileprivate 类成员
private func somePrivateMethod() {} // 显式 private 类成员
}

fileprivate class SomeFilePrivateClass { // 显式 fileprivate 类
func someFilePrivateMethod() {} // 隐式 fileprivate 类成员
private func somePrivateMethod() {} // 显式 private 类成员
}

private class SomePrivateClass { // 显式 private 类
func somePrivateMethod() {} // 隐式 private 类成员
}

元组类型

元组的访问级别将由元组中访问级别最严格的类型来决定。例如,如果你构建了一个包含两种不同类型的元组,其中一个类型为 internal,另一个类型为 private,那么这个元组的访问级别为 private

注意

元组不同于类、结构体、枚举、函数那样有单独的定义。元组的访问级别是在它被使用时自动推断出的,而无法明确指定。

函数类型

函数的访问级别根据访问级别最严格的参数类型或返回类型的访问级别来决定。但是,如果这种访问级别不符合函数定义所在环境的默认访问级别,那么就需要明确地指定该函数的访问级别。

下面的例子定义了一个名为 someFunction() 的全局函数,并且没有明确地指定其访问级别。也许你会认为该函数应该拥有默认的访问级别 internal,但事实并非如此。事实上,如果按下面这种写法,代码将无法通过编译:

1
2
3
func someFunction() -> (SomeInternalClass, SomePrivateClass) {
// 此处是函数实现部分
}

我们可以看到,这个函数的返回类型是一个元组,该元组中包含两个自定义的类(可查阅 自定义类型)。其中一个类的访问级别是 internal,另一个的访问级别是 private,所以根据元组访问级别的原则,该元组的访问级别是 private(元组的访问级别与元组中访问级别最低的类型一致)。

因为该函数返回类型的访问级别是 private,所以你必须使用 private 修饰符,明确指定该函数的访问级别:

1
2
3
private func someFunction() -> (SomeInternalClass, SomePrivateClass) {
// 此处是函数实现部分
}

将该函数指定为 publicinternal,或者使用默认的访问级别 internal 都是错误的,因为如果把该函数当做 publicinternal 级别来使用的话,可能会无法访问 private 级别的返回值。

枚举类型

枚举成员的访问级别和该枚举类型相同,你不能为枚举成员单独指定不同的访问级别。

比如下面的例子,枚举 CompassPoint 被明确指定为 public,那么它的成员 NorthSouthEastWest 的访问级别同样也是 public

1
2
3
4
5
6
public enum CompassPoint {
case north
case south
case east
case west
}

原始值和关联值

枚举定义中的任何原始值或关联值的类型的访问级别至少不能低于枚举类型的访问级别。例如,你不能在一个 internal 的枚举中定义 private 的原始值类型。

嵌套类型

如果在 private 的类型中定义嵌套类型,那么该嵌套类型就自动拥有 private 访问级别。如果在 public 或者 internal 级别的类型中定义嵌套类型,那么该嵌套类型自动拥有 internal 访问级别。如果想让嵌套类型拥有 public 访问级别,那么需要明确指定该嵌套类型的访问级别。

子类

子类的访问级别不得高于父类的访问级别。例如,父类的访问级别是 internal,子类的访问级别就不能是 public

此外,你可以在符合当前访问级别的条件下重写任意类成员(方法、属性、构造器、下标等)。

可以通过重写为继承来的类成员提供更高的访问级别。下面的例子中,类 A 的访问级别是 public,它包含一个方法 someMethod(),访问级别为 private。类 B 继承自类 A,访问级别为 internal,但是在类 B 中重写了类 A 中访问级别为 private 的方法 someMethod(),并重新指定为 internal 级别。通过这种方式,我们就可以将某类中 private 级别的类成员重新指定为更高的访问级别,以便其他人使用:

1
2
3
4
5
6
7
public class A {
fileprivate func someMethod() {}
}

internal class B: A {
override internal func someMethod() {}
}

我们甚至可以在子类中,用子类成员去访问访问级别更低的父类成员,只要这一操作在相应访问级别的限制范围内(也就是说,在同一源文件中访问父类 private 级别的成员,在同一模块内访问父类 internal 级别的成员):

1
2
3
4
5
6
7
8
9
public class A {
fileprivate func someMethod() {}
}

internal class B: A {
override internal func someMethod() {
super.someMethod()
}
}

因为父类 A 和子类 B 定义在同一个源文件中,所以在子类 B 可以在重写的 someMethod() 方法中调用 super.someMethod()

常量、变量、属性、下标

常量、变量、属性不能拥有比它们的类型更高的访问级别。例如,你不能定义一个 public 级别的属性,但是它的类型却是 private 级别的。同样,下标也不能拥有比索引类型或返回类型更高的访问级别。

如果常量、变量、属性、下标的类型是 private 级别的,那么它们必须明确指定访问级别为 private

1
private var privateInstance = SomePrivateClass()

Getter 和 Setter

常量、变量、属性、下标的 GettersSetters 的访问级别和它们所属类型的访问级别相同。

Setter 的访问级别可以低于对应的 Getter 的访问级别,这样就可以控制变量、属性或下标的读写权限。在 varsubscript 关键字之前,你可以通过 fileprivate(set)private(set)internal(set) 为它们的写入权限指定更低的访问级别。

注意

这个规则同时适用于存储型属性和计算型属性。即使你不明确指定存储型属性的 GetterSetter,Swift 也会隐式地为其创建 GetterSetter,用于访问该属性的后备存储。使用 fileprivate(set)private(set)internal(set) 可以改变 Setter 的访问级别,这对计算型属性也同样适用。

下面的例子中定义了一个名为 TrackedString 的结构体,它记录了 value 属性被修改的次数:

1
2
3
4
5
6
7
8
struct TrackedString {
private(set) var numberOfEdits = 0
var value: String = "" {
didSet {
numberOfEdits += 1
}
}
}

TrackedString 结构体定义了一个用于存储 String 值的属性 value,并将初始值设为 ""(一个空字符串)。该结构体还定义了另一个用于存储 Int 值的属性 numberOfEdits,它用于记录属性 value 被修改的次数。这个功能通过属性 valuedidSet 观察器实现,每当给 value 赋新值时就会调用 didSet 方法,然后将 numberOfEdits 的值加一。

结构体 TrackedString 和它的属性 value 都没有显式地指定访问级别,所以它们都是用默认的访问级别 internal。但是该结构体的 numberOfEdits 属性使用了 private(set) 修饰符,这意味着 numberOfEdits 属性只能在结构体的定义中进行赋值。numberOfEdits 属性的 Getter 依然是默认的访问级别 internal,但是 Setter 的访问级别是 private,这表示该属性只能在内部修改,而在结构体的外部则表现为一个只读属性。

如果你实例化 TrackedString 结构体,并多次对 value 属性的值进行修改,你就会看到 numberOfEdits 的值会随着修改次数而变化:

1
2
3
4
5
6
var stringToEdit = TrackedString()
stringToEdit.value = "This string will be tracked."
stringToEdit.value += " This edit will increment numberOfEdits."
stringToEdit.value += " So will this one."
print("The number of edits is \(stringToEdit.numberOfEdits)")
// 打印“The number of edits is 3”

虽然你可以在其他的源文件中实例化该结构体并且获取到 numberOfEdits 属性的值,但是你不能对其进行赋值。这一限制保护了该记录功能的实现细节,同时还提供了方便的访问方式。

你可以在必要时为 GetterSetter 显式指定访问级别。下面的例子将 TrackedString 结构体明确指定为了 public 访问级别。结构体的成员(包括 numberOfEdits 属性)拥有默认的访问级别 internal。你可以结合 publicprivate(set) 修饰符把结构体中的 numberOfEdits 属性的 Getter 的访问级别设置为 public,而 Setter 的访问级别设置为 private

1
2
3
4
5
6
7
8
9
public struct TrackedString {
public private(set) var numberOfEdits = 0
public var value: String = "" {
didSet {
numberOfEdits += 1
}
}
public init() {}
}

构造器

自定义构造器的访问级别可以低于或等于其所属类型的访问级别。唯一的例外是 必要构造器,它的访问级别必须和所属类型的访问级别相同。

如同函数或方法的参数,构造器参数的访问级别也不能低于构造器本身的访问级别。

默认构造器

默认构造器 所述,Swift 会为结构体和类提供一个默认的无参数的构造器,只要它们为所有存储型属性设置了默认初始值,并且未提供自定义的构造器。

默认构造器的访问级别与所属类型的访问级别相同,除非类型的访问级别是 public。如果一个类型被指定为 public 级别,那么默认构造器的访问级别将为 internal。如果你希望一个 public 级别的类型也能在其他模块中使用这种无参数的默认构造器,你只能自己提供一个 public 访问级别的无参数构造器。

结构体默认的成员逐一构造器

如果结构体中任意存储型属性的访问级别为 private,那么该结构体默认的成员逐一构造器的访问级别就是 private。否则,这种构造器的访问级别依然是 internal

如同前面提到的默认构造器,如果你希望一个 public 级别的结构体也能在其他模块中使用其默认的成员逐一构造器,你依然只能自己提供一个 public 访问级别的成员逐一构造器。

协议

如果想为一个协议类型明确地指定访问级别,在定义协议时指定即可。这将限制该协议只能在适当的访问级别范围内被遵循。

协议中的每一个要求都具有和该协议相同的访问级别。你不能将协议中的要求设置为其他访问级别。这样才能确保该协议的所有要求对于任意遵循者都将可用。

注意

如果你定义了一个 public 访问级别的协议,那么该协议的所有实现也会是 public 访问级别。这一点不同于其他类型,例如,当类型是 public 访问级别时,其成员的访问级别却只是 internal

协议继承

如果定义了一个继承自其他协议的新协议,那么新协议拥有的访问级别最高也只能和被继承协议的访问级别相同。例如,你不能将继承自 internal 协议的新协议定义为 public 协议。

协议遵循

一个类型可以遵循比它级别更低的协议。例如,你可以定义一个 public 级别类型,它能在别的模块中使用,但是如果它遵循一个 internal 协议,这个遵循的部分就只能在这个 internal 协议所在的模块中使用。

遵循协议时的上下文级别是类型和协议中级别最小的那个。如果一个类型是 public 级别,但它要遵循的协议是 internal 级别,那么这个类型对该协议的遵循上下文就是 internal 级别。

当你编写或扩展一个类型让它遵循一个协议时,你必须确保该类型对协议的每一个要求的实现,至少与遵循协议的上下文级别一致。例如,一个 public 类型遵循一个 internal 协议,这个类型对协议的所有实现至少都应是 internal 级别的。

注意

Swift 和 Objective-C 一样,协议遵循是全局的,也就是说,在同一程序中,一个类型不可能用两种不同的方式实现同一个协议。

Extension

Extension 可以在访问级别允许的情况下对类、结构体、枚举进行扩展。Extension 的成员具有和原始类型成员一致的访问级别。例如,你使用 extension 扩展了一个 public 或者 internal 类型,extension 中的成员就默认使用 internal 访问级别,和原始类型中的成员一致。如果你使用 extension 扩展了一个 private 类型,则 extension 的成员默认使用 private 访问级别。

或者,你可以明确指定 extension 的访问级别(例如,private extension),从而给该 extension 中的所有成员指定一个新的默认访问级别。这个新的默认访问级别仍然可以被单独指定的访问级别所覆盖。

如果你使用 extension 来遵循协议的话,就不能显式地声明 extension 的访问级别。extension 每个 protocol 要求的实现都默认使用 protocol 的访问级别。

Extension 的私有成员

扩展同一文件内的类,结构体或者枚举,extension 里的代码会表现得跟声明在原类型里的一模一样。也就是说你可以这样:

  • 在类型的声明里声明一个私有成员,在同一文件的 extension 里访问。
  • 在 extension 里声明一个私有成员,在同一文件的另一个 extension 里访问。
  • 在 extension 里声明一个私有成员,在同一文件的类型声明里访问。

这意味着你可以像组织的代码去使用 extension,而且不受私有成员的影响。例如,给定下面这样一个简单的协议:

1
2
3
protocol SomeProtocol {
func doSomething()
}

你可以使用 extension 来遵循协议,就像这样:

1
2
3
4
5
6
7
8
9
struct SomeStruct {
private var privateVariable = 12
}

extension SomeStruct: SomeProtocol {
func doSomething() {
print(privateVariable)
}
}

泛型

泛型类型或泛型函数的访问级别取决于泛型类型或泛型函数本身的访问级别,还需结合类型参数的类型约束的访问级别,根据这些访问级别中的最低访问级别来确定。

类型别名

你定义的任何类型别名都会被当作不同的类型,以便于进行访问控制。类型别名的访问级别不可高于其表示的类型的访问级别。例如,private 级别的类型别名可以作为 privatefile-privateinternalpublic 或者 open 类型的别名,但是 public 级别的类型别名只能作为 public 类型的别名,不能作为 internalfile-privateprivate 类型的别名。

注意

这条规则也适用于为满足协议遵循而将类型别名用于关联类型的情况。

留言與分享

swift内存安全

分類 编程语言, swift

内存安全

默认情况下,Swift 会阻止你代码里不安全的行为。例如,Swift 会保证变量在使用之前就完成初始化,在内存被回收之后就无法被访问,并且数组的索引会做越界检查。

Swift 也保证同时访问同一块内存时不会冲突,通过约束代码里对于存储地址的写操作,去获取那一块内存的访问独占权。因为 Swift 自动管理内存,所以大部分时候你完全不需要考虑内存访问的事情。然而,理解潜在的冲突也是很重要的,可以避免你写出访问冲突的代码。而如果你的代码确实存在冲突,那在编译时或者运行时就会得到错误。

理解内存访问冲突

内存的访问,会发生在你给变量赋值,或者传递参数给函数时。例如,下面的代码就包含了读和写的访问:

1
2
3
4
5
// 向 one 所在的内存区域发起一次写操作
var one = 1

// 向 one 所在的内存区域发起一次读操作
print("We're number \(one)!")

内存访问的冲突会发生在你的代码尝试同时访问同一个存储地址的时侯。同一个存储地址的多个访问同时发生会造成不可预计或不一致的行为。在 Swift 里,有很多修改值的行为都会持续好几行代码,在修改值的过程中进行访问是有可能发生的。

你可以思考一下预算表更新的过程,会看到同样的问题。更新预算表总共有两步:首先你把预算项的名字和费用加上,然后再更新总数来反映预算表的现况。在更新之前和之后,你都可以从预算表里读取任何信息并获得正确的答案,就像下面展示的那样。

而当你添加预算项进入表里的时候,它只是在一个临时的,错误的状态,因为总数还没有被更新。在添加数据的过程中读取总数就会读取到错误的信息。

这个例子也演示了你在修复内存访问冲突时会遇到的问题:有时修复的方式会有很多种,但哪一种是正确的就不总是那么明显了。在这个例子里,根据你是否需要更新后的总数,$5 和 $320 都可能是正确的值。在你修复访问冲突之前,你需要决定它的倾向。

注意

如果你写过并发和多线程的代码,内存访问冲突也许是同样的问题。然而,这里访问冲突的讨论是在单线程的情境下讨论的,并没有使用并发或者多线程。

如果你曾经在单线程代码里有访问冲突,Swift 可以保证你在编译或者运行时会得到错误。对于多线程的代码,可以使用 Thread Sanitizer 去帮助检测多线程的冲突。

内存访问性质

内存访问冲突时,要考虑内存访问上下文中的这三个性质:访问是读还是写,访问的时长,以及被访问的存储地址。特别是,冲突会发生在当你有两个访问符合下列的情况:

  • 至少有一个是写访问
  • 它们访问的是同一个存储地址
  • 它们的访问在时间线上部分重叠

读和写访问的区别很明显:一个写访问会改变存储地址,而读操作不会。存储地址是指向正在访问的东西(例如一个变量,常量或者属性)的位置的值 。内存访问的时长要么是瞬时的,要么是长期的。

如果一个访问不可能在其访问期间被其它代码访问,那么就是一个瞬时访问。正常来说,两个瞬时访问是不可能同时发生的。大多数内存访问都是瞬时的。例如,下面列举的所有读和写访问都是瞬时的:

1
2
3
4
5
6
7
8
func oneMore(than number: Int) -> Int {
return number + 1
}

var myNumber = 1
myNumber = oneMore(than: myNumber)
print(myNumber)
// 打印“2”

然而,有几种被称为长期访问的内存访问方式,会在别的代码执行时持续进行。瞬时访问和长期访问的区别在于别的代码有没有可能在访问期间同时访问,也就是在时间线上的重叠。一个长期访问可以被别的长期访问或瞬时访问重叠。

重叠的访问主要出现在使用 in-out 参数的函数和方法或者结构体的 mutating 方法里。Swift 代码里典型的长期访问会在后面进行讨论。

In-Out 参数的访问冲突

一个函数会对它所有的 in-out 参数进行长期写访问。in-out 参数的写访问会在所有非 in-out 参数处理完之后开始,直到函数执行完毕为止。如果有多个 in-out 参数,则写访问开始的顺序与参数的顺序一致。

长期访问的存在会造成一个结果,你不能在访问以 in-out 形式传入后的原变量,即使作用域原则和访问权限允许——任何访问原变量的行为都会造成冲突。例如:

1
2
3
4
5
6
7
8
var stepSize = 1

func increment(_ number: inout Int) {
number += stepSize
}

increment(&stepSize)
// 错误:stepSize 访问冲突

在上面的代码里,stepSize 是一个全局变量,并且它可以在 increment(_:) 里正常访问。然而,对于 stepSize 的读访问与 number 的写访问重叠了。就像下面展示的那样,numberstepSize 都指向了同一个存储地址。同一块内存的读和写访问重叠了,就此产生了冲突。

解决这个冲突的一种方式,是显示拷贝一份 stepSize

1
2
3
4
5
6
7
// 显式拷贝
var copyOfStepSize = stepSize
increment(&copyOfStepSize)

// 更新原来的值
stepSize = copyOfStepSize
// stepSize 现在的值是 2

当你在调用 increment(_:) 之前做一份拷贝,显然 copyOfStepSize 就会根据当前的 stepSize 增加。读访问在写操作之前就已经结束了,所以不会有冲突。

长期写访问的存在还会造成另一种结果,往同一个函数的多个 in-out 参数里传入同一个变量也会产生冲突,例如:

1
2
3
4
5
6
7
8
9
10
func balance(_ x: inout Int, _ y: inout Int) {
let sum = x + y
x = sum / 2
y = sum - x
}
var playerOneScore = 42
var playerTwoScore = 30
balance(&playerOneScore, &playerTwoScore) // 正常
balance(&playerOneScore, &playerOneScore)
// 错误:playerOneScore 访问冲突

上面的 balance(_:_:) 函数会将传入的两个参数平均化。将 playerOneScoreplayerTwoScore 作为参数传入不会产生错误 —— 有两个访问重叠了,但它们访问的是不同的内存位置。相反,将 playerOneScore 作为参数同时传入就会产生冲突,因为它会发起两个写访问,同时访问同一个的存储地址。

注意

因为操作符也是函数,它们也会对 in-out 参数进行长期访问。例如,假设 balance(_:_:) 是一个名为 <^> 的操作符函数,那么 playerOneScore <^> playerOneScore 也会造成像 balance(&playerOneScore, &playerOneScore) 一样的冲突。

方法里 self 的访问冲突

一个结构体的 mutating 方法会在调用期间对 self 进行写访问。例如,想象一下这么一个游戏,每一个玩家都有血量,受攻击时血量会下降,并且有敌人的数量,使用特殊技能时会减少敌人数量。

1
2
3
4
5
6
7
8
9
10
struct Player {
var name: String
var health: Int
var energy: Int

static let maxHealth = 10
mutating func restoreHealth() {
health = Player.maxHealth
}
}

在上面的 restoreHealth() 方法里,一个对于 self 的写访问会从方法开始直到方法 return。在这种情况下,restoreHealth() 里的其它代码不可以对 Player 实例的属性发起重叠的访问。下面的 shareHealth(with:) 方法接受另一个 Player 的实例作为 in-out 参数,产生了访问重叠的可能性。

1
2
3
4
5
6
7
8
9
extension Player {
mutating func shareHealth(with teammate: inout Player) {
balance(&teammate.health, &health)
}
}

var oscar = Player(name: "Oscar", health: 10, energy: 10)
var maria = Player(name: "Maria", health: 5, energy: 10)
oscar.shareHealth(with: &maria) // 正常

上面的例子里,调用 shareHealth(with:) 方法去把 oscar 玩家的血量分享给 maria 玩家并不会造成冲突。在方法调用期间会对 oscar 发起写访问,因为在 mutating 方法里 self 就是 oscar,同时对于 maria 也会发起写访问,因为 maria 作为 in-out 参数传入。过程如下,它们会访问内存的不同位置。即使两个写访问重叠了,它们也不会冲突。

当然,如果你将 oscar 作为参数传入 shareHealth(with:) 里,就会产生冲突:

1
2
oscar.shareHealth(with: &oscar)
// 错误:oscar 访问冲突

mutating 方法在调用期间需要对 self 发起写访问,而同时 in-out 参数也需要写访问。在方法里,selfteammate 都指向了同一个存储地址——就像下面展示的那样。对于同一块内存同时进行两个写访问,并且它们重叠了,就此产生了冲突。

属性的访问冲突

如结构体,元组和枚举的类型都是由多个独立的值组成的,例如结构体的属性或元组的元素。因为它们都是值类型,修改值的任何一部分都是对于整个值的修改,意味着其中一个属性的读或写访问都需要访问整一个值。例如,元组元素的写访问重叠会产生冲突:

1
2
3
var playerInformation = (health: 10, energy: 20)
balance(&playerInformation.health, &playerInformation.energy)
// 错误:playerInformation 的属性访问冲突

上面的例子里,传入同一元组的元素对 balance(_:_:) 进行调用,产生了冲突,因为 playerInformation 的访问产生了写访问重叠。playerInformation.healthplayerInformation.energy 都被作为 in-out 参数传入,意味着 balance(_:_:) 需要在函数调用期间对它们发起写访问。任何情况下,对于元组元素的写访问都需要对整个元组发起写访问。这意味着对于 playerInfomation 发起的两个写访问重叠了,造成冲突。

下面的代码展示了一样的错误,对于一个存储在全局变量里的结构体属性的写访问重叠了。

1
2
var holly = Player(name: "Holly", health: 10, energy: 10)
balance(&holly.health, &holly.energy) // 错误

在实践中,大多数对于结构体属性的访问都会安全的重叠。例如,将上面例子里的变量 holly 改为本地变量而非全局变量,编译器就会可以保证这个重叠访问是安全的:

1
2
3
4
func someFunction() {
var oscar = Player(name: "Oscar", health: 10, energy: 10)
balance(&oscar.health, &oscar.energy) // 正常
}

上面的例子里,oscarhealthenergy 都作为 in-out 参数传入了 balance(_:_:) 里。编译器可以保证内存安全,因为两个存储属性任何情况下都不会相互影响。

限制结构体属性的重叠访问对于保证内存安全不是必要的。保证内存安全是必要的,但因为访问独占权的要求比内存安全还要更严格——意味着即使有些代码违反了访问独占权的原则,也是内存安全的,所以如果编译器可以保证这种非专属的访问是安全的,那 Swift 就会允许这种行为的代码运行。特别是当你遵循下面的原则时,它可以保证结构体属性的重叠访问是安全的:

  • 你访问的是实例的存储属性,而不是计算属性或类的属性
  • 结构体是本地变量的值,而非全局变量
  • 结构体要么没有被闭包捕获,要么只被非逃逸闭包捕获了

如果编译器无法保证访问的安全性,它就不会允许那次访问。

留言與分享

swift自动引用计数

分類 编程语言, swift

自动引用计数

Swift 使用*自动引用计数(ARC)*机制来跟踪和管理你的应用程序的内存。通常情况下,Swift 内存管理机制会一直起作用,你无须自己来考虑内存的管理。ARC 会在类的实例不再被使用时,自动释放其占用的内存。

然而在少数情况下,为了能帮助你管理内存,ARC 需要更多的,代码之间关系的信息。本章描述了这些情况,并且为你示范怎样才能使 ARC 来管理你的应用程序的所有内存。在 Swift 使用 ARC 与在 Obejctive-C 中使用 ARC 非常类似,具体请参考 过渡到 ARC 的发布说明

注意

引用计数仅仅应用于类的实例。结构体和枚举类型是值类型,不是引用类型,也不是通过引用的方式存储和传递。

自动引用计数的工作机制

当你每次创建一个类的新的实例的时候,ARC 会分配一块内存来储存该实例信息。内存中会包含实例的类型信息,以及这个实例所有相关的存储型属性的值。

此外,当实例不再被使用时,ARC 释放实例所占用的内存,并让释放的内存能挪作他用。这确保了不再被使用的实例,不会一直占用内存空间。

然而,当 ARC 收回和释放了正在被使用中的实例,该实例的属性和方法将不能再被访问和调用。实际上,如果你试图访问这个实例,你的应用程序很可能会崩溃。

为了确保使用中的实例不会被销毁,ARC 会跟踪和计算每一个实例正在被多少属性,常量和变量所引用。哪怕实例的引用数为 1,ARC 都不会销毁这个实例。

为了使上述成为可能,无论你将实例赋值给属性、常量或变量,它们都会创建此实例的强引用。之所以称之为“强”引用,是因为它会将实例牢牢地保持住,只要强引用还在,实例是不允许被销毁的。

自动引用计数实践

下面的例子展示了自动引用计数的工作机制。例子以一个简单的 Person 类开始,并定义了一个叫 name 的常量属性:

1
2
3
4
5
6
7
8
9
10
class Person {
let name: String
init(name: String) {
self.name = name
print("\(name) is being initialized")
}
deinit {
print("\(name) is being deinitialized")
}
}

Person 类有一个构造器,此构造器为实例的 name 属性赋值,并打印一条消息以表明初始化过程生效。Person 类也拥有一个析构器,这个析构器会在实例被销毁时打印一条消息。

接下来的代码片段定义了三个类型为 Person? 的变量,用来按照代码片段中的顺序,为新的 Person 实例建立多个引用。由于这些变量是被定义为可选类型(Person?,而不是 Person),它们的值会被自动初始化为 nil,目前还不会引用到 Person 类的实例。

1
2
3
var reference1: Person?
var reference2: Person?
var reference3: Person?

现在你可以创建 Person 类的新实例,并且将它赋值给三个变量中的一个:

1
2
reference1 = Person(name: "John Appleseed")
// 打印“John Appleseed is being initialized”

应当注意到当你调用 Person 类的构造器的时候,"John Appleseed is being initialized" 会被打印出来。由此可以确定构造器被执行。

由于 Person 类的新实例被赋值给了 reference1 变量,所以 reference1Person 类的新实例之间建立了一个强引用。正是因为这一个强引用,ARC 会保证 Person 实例被保持在内存中不被销毁。

如果你将同一个 Person 实例也赋值给其他两个变量,该实例又会多出两个强引用:

1
2
reference2 = reference1
reference3 = reference1

现在这一个 Person 实例已经有三个强引用了。

如果你通过给其中两个变量赋值 nil 的方式断开两个强引用(包括最先的那个强引用),只留下一个强引用,Person 实例不会被销毁:

1
2
reference1 = nil
reference2 = nil

在你清楚地表明不再使用这个 Person 实例时,即第三个也就是最后一个强引用被断开时,ARC 会销毁它:

1
2
reference3 = nil
// 打印“John Appleseed is being deinitialized”

类实例之间的循环强引用

在上面的例子中,ARC 会跟踪你所新创建的 Person 实例的引用数量,并且会在 Person 实例不再被需要时销毁它。

然而,我们可能会写出一个类实例的强引用数永远不能变成 0 的代码。如果两个类实例互相持有对方的强引用,因而每个实例都让对方一直存在,就是这种情况。这就是所谓的循环强引用

你可以通过定义类之间的关系为弱引用或无主引用,以替代强引用,从而解决循环强引用的问题。具体的过程在 解决类实例之间的循环强引用 中有描述。不管怎样,在你学习怎样解决循环强引用之前,很有必要了解一下它是怎样产生的。

下面展示了一个不经意产生循环强引用的例子。例子定义了两个类:PersonApartment,用来建模公寓和它其中的居民:

1
2
3
4
5
6
7
8
9
10
11
12
13
class Person {
let name: String
init(name: String) { self.name = name }
var apartment: Apartment?
deinit { print("\(name) is being deinitialized") }
}

class Apartment {
let unit: String
init(unit: String) { self.unit = unit }
var tenant: Person?
deinit { print("Apartment \(unit) is being deinitialized") }
}

每一个 Person 实例有一个类型为 String,名字为 name 的属性,并有一个可选的初始化为 nilapartment 属性。apartment 属性是可选的,因为一个人并不总是拥有公寓。

类似的,每个 Apartment 实例有一个叫 unit,类型为 String 的属性,并有一个可选的初始化为 niltenant 属性。tenant 属性是可选的,因为一栋公寓并不总是有居民。

这两个类都定义了析构器,用以在类实例被析构的时候输出信息。这让你能够知晓 PersonApartment 的实例是否像预期的那样被销毁。

接下来的代码片段定义了两个可选类型的变量 johnunit4A,并分别被设定为下面的 ApartmentPerson 的实例。这两个变量都被初始化为 nil,这正是可选类型的优点:

1
2
var john: Person?
var unit4A: Apartment?

现在你可以创建特定的 PersonApartment 实例并将赋值给 johnunit4A 变量:

1
2
john = Person(name: "John Appleseed")
unit4A = Apartment(unit: "4A")

在两个实例被创建和赋值后,下图表现了强引用的关系。变量 john 现在有一个指向 Person 实例的强引用,而变量 unit4A 有一个指向 Apartment 实例的强引用:

现在你能够将这两个实例关联在一起,这样人就能有公寓住了,而公寓也有了房客。注意感叹号是用来展开和访问可选变量 johnunit4A 中的实例,这样实例的属性才能被赋值:

1
2
john!.apartment = unit4A
unit4A!.tenant = john

在将两个实例联系在一起之后,强引用的关系如图所示:

不幸的是,这两个实例关联后会产生一个循环强引用。Person 实例现在有了一个指向 Apartment 实例的强引用,而 Apartment 实例也有了一个指向 Person 实例的强引用。因此,当你断开 johnunit4A 变量所持有的强引用时,引用计数并不会降为 0,实例也不会被 ARC 销毁:

1
2
john = nil
unit4A = nil

注意,当你把这两个变量设为 nil 时,没有任何一个析构器被调用。循环强引用会一直阻止 PersonApartment 类实例的销毁,这就在你的应用程序中造成了内存泄漏。

在你将 johnunit4A 赋值为 nil 后,强引用关系如下图:

PersonApartment 实例之间的强引用关系保留了下来并且不会被断开。

解决实例之间的循环强引用

Swift 提供了两种办法用来解决你在使用类的属性时所遇到的循环强引用问题:弱引用(weak reference)和无主引用(unowned reference)。

弱引用和无主引用允许循环引用中的一个实例引用另一个实例而保持强引用。这样实例能够互相引用而不产生循环强引用。

当其他的实例有更短的生命周期时,使用弱引用,也就是说,当其他实例析构在先时。在上面公寓的例子中,很显然一个公寓在它的生命周期内会在某个时间段没有它的主人,所以一个弱引用就加在公寓类里面,避免循环引用。相比之下,当其他实例有相同的或者更长生命周期时,请使用无主引用。

弱引用

弱引用不会对其引用的实例保持强引用,因而不会阻止 ARC 销毁被引用的实例。这个特性阻止了引用变为循环强引用。声明属性或者变量时,在前面加上 weak 关键字表明这是一个弱引用。

因为弱引用不会保持所引用的实例,即使引用存在,实例也有可能被销毁。因此,ARC 会在引用的实例被销毁后自动将其弱引用赋值为 nil。并且因为弱引用需要在运行时允许被赋值为 nil,所以它们会被定义为可选类型变量,而不是常量。

你可以像其他可选值一样,检查弱引用的值是否存在,你将永远不会访问已销毁的实例的引用。

注意

当 ARC 设置弱引用为 nil 时,属性观察不会被触发。

下面的例子跟上面 PersonApartment 的例子一致,但是有一个重要的区别。这一次,Apartmenttenant 属性被声明为弱引用:

1
2
3
4
5
6
7
8
9
10
11
12
13
class Person {
let name: String
init(name: String) { self.name = name }
var apartment: Apartment?
deinit { print("\(name) is being deinitialized") }
}

class Apartment {
let unit: String
init(unit: String) { self.unit = unit }
weak var tenant: Person?
deinit { print("Apartment \(unit) is being deinitialized") }
}

然后跟之前一样,建立两个变量(johnunit4A)之间的强引用,并关联两个实例:

1
2
3
4
5
6
7
8
var john: Person?
var unit4A: Apartment?

john = Person(name: "John Appleseed")
unit4A = Apartment(unit: "4A")

john!.apartment = unit4A
unit4A!.tenant = john

现在,两个关联在一起的实例的引用关系如下图所示:

Person 实例依然保持对 Apartment 实例的强引用,但是 Apartment 实例只持有对 Person 实例的弱引用。这意味着当你通过把 john 变量赋值为 nil 而断开其所保持的强引用时,再也没有指向 Person 实例的强引用了:

1
2
john = nil
// 打印“John Appleseed is being deinitialized”

由于再也没有指向 Person 实例的强引用,该实例会被销毁,且 tenant 属性会被赋值为 nil

唯一剩下的指向 Apartment 实例的强引用来自于变量 unit4A。如果你断开这个强引用,再也没有指向 Apartment 实例的强引用了:

1
2
unit4A = nil
// 打印“Apartment 4A is being deinitialized”

由于再也没有指向 Person 实例的强引用,该实例会被销毁:

注意

在使用垃圾收集的系统里,弱指针有时用来实现简单的缓冲机制,因为没有强引用的对象只会在内存压力触发垃圾收集时才被销毁。但是在 ARC 中,一旦值的最后一个强引用被移除,就会被立即销毁,这导致弱引用并不适合上面的用途。

无主引用

和弱引用类似,无主引用不会牢牢保持住引用的实例。和弱引用不同的是,无主引用在其他实例有相同或者更长的生命周期时使用。你可以在声明属性或者变量时,在前面加上关键字 unowned 表示这是一个无主引用。

无主引用通常都被期望拥有值。不过 ARC 无法在实例被销毁后将无主引用设为 nil,因为非可选类型的变量不允许被赋值为 nil

重点

使用无主引用,你必须确保引用始终指向一个未销毁的实例。

如果你试图在实例被销毁后,访问该实例的无主引用,会触发运行时错误。

下面的例子定义了两个类,CustomerCreditCard,模拟了银行客户和客户的信用卡。这两个类中,每一个都将另外一个类的实例作为自身的属性。这种关系可能会造成循环强引用。

CustomerCreditCard 之间的关系与前面弱引用例子中 ApartmentPerson 的关系略微不同。在这个数据模型中,一个客户可能有或者没有信用卡,但是一张信用卡总是关联着一个客户。为了表示这种关系,Customer 类有一个可选类型的 card 属性,但是 CreditCard 类有一个非可选类型的 customer 属性。

此外,只能通过将一个 number 值和 customer 实例传递给 CreditCard 构造器的方式来创建 CreditCard 实例。这样可以确保当创建 CreditCard 实例时总是有一个 customer 实例与之关联。

由于信用卡总是关联着一个客户,因此将 customer 属性定义为无主引用,用以避免循环强引用:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class Customer {
let name: String
var card: CreditCard?
init(name: String) {
self.name = name
}
deinit { print("\(name) is being deinitialized") }
}

class CreditCard {
let number: UInt64
unowned let customer: Customer
init(number: UInt64, customer: Customer) {
self.number = number
self.customer = customer
}
deinit { print("Card #\(number) is being deinitialized") }
}

注意

CreditCard 类的 number 属性被定义为 UInt64 类型而不是 Int 类型,以确保 number 属性的存储量在 32 位和 64 位系统上都能足够容纳 16 位的卡号。

下面的代码片段定义了一个叫 john 的可选类型 Customer 变量,用来保存某个特定客户的引用。由于是可选类型,所以变量被初始化为 nil

1
var john: Customer?

现在你可以创建 Customer 类的实例,用它初始化 CreditCard 实例,并将新创建的 CreditCard 实例赋值为客户的 card 属性:

1
2
john = Customer(name: "John Appleseed")
john!.card = CreditCard(number: 1234_5678_9012_3456, customer: john!)

在你关联两个实例后,它们的引用关系如下图所示:

Customer 实例持有对 CreditCard 实例的强引用,而 CreditCard 实例持有对 Customer 实例的无主引用。

由于 customer 的无主引用,当你断开 john 变量持有的强引用时,再也没有指向 Customer 实例的强引用了:

由于再也没有指向 Customer 实例的强引用,该实例被销毁了。其后,再也没有指向 CreditCard 实例的强引用,该实例也随之被销毁了:

1
2
3
john = nil
// 打印“John Appleseed is being deinitialized”
// 打印“Card #1234567890123456 is being deinitialized”

最后的代码展示了在 john 变量被设为 nilCustomer 实例和 CreditCard 实例的析构器都打印出了“销毁”的信息。

注意

上面的例子展示了如何使用安全的无主引用。对于需要禁用运行时的安全检查的情况(例如,出于性能方面的原因),Swift 还提供了不安全的无主引用。与所有不安全的操作一样,你需要负责检查代码以确保其安全性。 你可以通过 unowned(unsafe) 来声明不安全无主引用。如果你试图在实例被销毁后,访问该实例的不安全无主引用,你的程序会尝试访问该实例之前所在的内存地址,这是一个不安全的操作。

无主引用和隐式解包可选值属性

上面弱引用和无主引用的例子涵盖了两种常用的需要打破循环强引用的场景。

PersonApartment 的例子展示了两个属性的值都允许为 nil,并会潜在的产生循环强引用。这种场景最适合用弱引用来解决。

CustomerCreditCard 的例子展示了一个属性的值允许为 nil,而另一个属性的值不允许为 nil,这也可能会产生循环强引用。这种场景最适合通过无主引用来解决。

然而,存在着第三种场景,在这种场景中,两个属性都必须有值,并且初始化完成后永远不会为 nil。在这种场景中,需要一个类使用无主属性,而另外一个类使用隐式解包可选值属性。

这使两个属性在初始化完成后能被直接访问(不需要可选展开),同时避免了循环引用。这一节将为你展示如何建立这种关系。

下面的例子定义了两个类,CountryCity,每个类将另外一个类的实例保存为属性。在这个模型中,每个国家必须有首都,每个城市必须属于一个国家。为了实现这种关系,Country 类拥有一个 capitalCity 属性,而 City 类有一个 country 属性:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class Country {
let name: String
var capitalCity: City!
init(name: String, capitalName: String) {
self.name = name
self.capitalCity = City(name: capitalName, country: self)
}
}

class City {
let name: String
unowned let country: Country
init(name: String, country: Country) {
self.name = name
self.country = country
}
}

为了建立两个类的依赖关系,City 的构造器接受一个 Country 实例作为参数,并且将实例保存到 country 属性。

Country 的构造器调用了 City 的构造器。然而,只有 Country 的实例完全初始化后,Country 的构造器才能把 self 传给 City 的构造器。在 两段式构造过程 中有具体描述。

为了满足这种需求,通过在类型结尾处加上感叹号(City!)的方式,将 CountrycapitalCity 属性声明为隐式解包可选值类型的属性。这意味着像其他可选类型一样,capitalCity 属性的默认值为 nil,但是不需要展开它的值就能访问它。在 隐式解包可选值 中有描述。

由于 capitalCity 默认值为 nil,一旦 Country 的实例在构造器中给 name 属性赋值后,整个初始化过程就完成了。这意味着一旦 name 属性被赋值后,Country 的构造器就能引用并传递隐式的 selfCountry 的构造器在赋值 capitalCity 时,就能将 self 作为参数传递给 City 的构造器。

以上的意义在于你可以通过一条语句同时创建 CountryCity 的实例,而不产生循环强引用,并且 capitalCity 的属性能被直接访问,而不需要通过感叹号来展开它的可选值:

1
2
3
var country = Country(name: "Canada", capitalName: "Ottawa")
print("\(country.name)'s capital city is called \(country.capitalCity.name)")
// 打印“Canada's capital city is called Ottawa”

在上面的例子中,使用隐式解包可选值值意味着满足了类的构造器的两个构造阶段的要求。capitalCity 属性在初始化完成后,能像非可选值一样使用和存取,同时还避免了循环强引用。

闭包的循环强引用

前面我们看到了循环强引用是在两个类实例属性互相保持对方的强引用时产生的,还知道了如何用弱引用和无主引用来打破这些循环强引用。

循环强引用还会发生在当你将一个闭包赋值给类实例的某个属性,并且这个闭包体中又使用了这个类实例时。这个闭包体中可能访问了实例的某个属性,例如 self.someProperty,或者闭包中调用了实例的某个方法,例如 self.someMethod()。这两种情况都导致了闭包“捕获”self,从而产生了循环强引用。

循环强引用的产生,是因为闭包和类相似,都是引用类型。当你把一个闭包赋值给某个属性时,你是将这个闭包的引用赋值给了属性。实质上,这跟之前的问题是一样的——两个强引用让彼此一直有效。但是,和两个类实例不同,这次一个是类实例,另一个是闭包。

Swift 提供了一种优雅的方法来解决这个问题,称之为 闭包捕获列表(closure capture list)。同样的,在学习如何用闭包捕获列表打破循环强引用之前,先来了解一下这里的循环强引用是如何产生的,这对我们很有帮助。

下面的例子为你展示了当一个闭包引用了 self 后是如何产生一个循环强引用的。例子中定义了一个叫 HTMLElement 的类,用一种简单的模型表示 HTML 文档中的一个单独的元素:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
class HTMLElement {

let name: String
let text: String?

lazy var asHTML: () -> String = {
if let text = self.text {
return "<\(self.name)>\(text)</\(self.name)>"
} else {
return "<\(self.name) />"
}
}

init(name: String, text: String? = nil) {
self.name = name
self.text = text
}

deinit {
print("\(name) is being deinitialized")
}

}

HTMLElement 类定义了一个 name 属性来表示这个元素的名称,例如代表头部元素的 "h1",代表段落的 "p",或者代表换行的 "br"HTMLElement 还定义了一个可选属性 text,用来设置 HTML 元素呈现的文本。

除了上面的两个属性,HTMLElement 还定义了一个 lazy 属性 asHTML。这个属性引用了一个将 nametext 组合成 HTML 字符串片段的闭包。该属性是 Void -> String 类型,或者可以理解为“一个没有参数,返回 String 的函数”。

默认情况下,闭包赋值给了 asHTML 属性,这个闭包返回一个代表 HTML 标签的字符串。如果 text 值存在,该标签就包含可选值 text;如果 text 不存在,该标签就不包含文本。对于段落元素,根据 text"some text" 还是 nil,闭包会返回 "<p>some text</p>" 或者 "<p />"

可以像实例方法那样去命名、使用 asHTML 属性。然而,由于 asHTML 是闭包而不是实例方法,如果你想改变特定 HTML 元素的处理方式的话,可以用自定义的闭包来取代默认值。

例如,可以将一个闭包赋值给 asHTML 属性,这个闭包能在 text 属性是 nil 时使用默认文本,这是为了避免返回一个空的 HTML 标签:

1
2
3
4
5
6
7
let heading = HTMLElement(name: "h1")
let defaultText = "some default text"
heading.asHTML = {
return "<\(heading.name)>\(heading.text ?? defaultText)</\(heading.name)>"
}
print(heading.asHTML())
// 打印“<h1>some default text</h1>”

注意

asHTML 声明为 lazy 属性,因为只有当元素确实需要被处理为 HTML 输出的字符串时,才需要使用 asHTML。也就是说,在默认的闭包中可以使用 self,因为只有当初始化完成以及 self 确实存在后,才能访问 lazy 属性。

HTMLElement 类只提供了一个构造器,通过 nametext(如果有的话)参数来初始化一个新元素。该类也定义了一个析构器,当 HTMLElement 实例被销毁时,打印一条消息。

下面的代码展示了如何用 HTMLElement 类创建实例并打印消息:

1
2
3
var paragraph: HTMLElement? = HTMLElement(name: "p", text: "hello, world")
print(paragraph!.asHTML())
// 打印“<p>hello, world</p>”

注意

上面的 paragraph 变量定义为可选类型的 HTMLElement,因此我们可以赋值 nil 给它来演示循环强引用。

不幸的是,上面写的 HTMLElement 类产生了类实例和作为 asHTML 默认值的闭包之间的循环强引用。循环强引用如下图所示:

实例的 asHTML 属性持有闭包的强引用。但是,闭包在其闭包体内使用了 self(引用了 self.nameself.text),因此闭包捕获了 self,这意味着闭包又反过来持有了 HTMLElement 实例的强引用。这样两个对象就产生了循环强引用。(更多关于闭包捕获值的信息,请参考 值捕获)。

注意

虽然闭包多次使用了 self,它只捕获 HTMLElement 实例的一个强引用。

如果设置 paragraph 变量为 nil,打破它持有的 HTMLElement 实例的强引用,HTMLElement 实例和它的闭包都不会被销毁,也是因为循环强引用:

1
paragraph = nil

注意,HTMLElement 的析构器中的消息并没有被打印,证明了 HTMLElement 实例并没有被销毁。

解决闭包的循环强引用

在定义闭包时同时定义捕获列表作为闭包的一部分,通过这种方式可以解决闭包和类实例之间的循环强引用。捕获列表定义了闭包体内捕获一个或者多个引用类型的规则。跟解决两个类实例间的循环强引用一样,声明每个捕获的引用为弱引用或无主引用,而不是强引用。应当根据代码关系来决定使用弱引用还是无主引用。

注意

Swift 有如下要求:只要在闭包内使用 self 的成员,就要用 self.someProperty 或者 self.someMethod()(而不只是 somePropertysomeMethod())。这提醒你可能会一不小心就捕获了 self

定义捕获列表

捕获列表中的每一项都由一对元素组成,一个元素是 weakunowned 关键字,另一个元素是类实例的引用(例如 self)或初始化过的变量(如 delegate = self.delegate!)。这些项在方括号中用逗号分开。

如果闭包有参数列表和返回类型,把捕获列表放在它们前面:

1
2
3
4
lazy var someClosure: (Int, String) -> String = {
[unowned self, weak delegate = self.delegate!] (index: Int, stringToProcess: String) -> String in
// 这里是闭包的函数体
}

如果闭包没有指明参数列表或者返回类型,它们会通过上下文推断,那么可以把捕获列表和关键字 in 放在闭包最开始的地方:

1
2
3
4
lazy var someClosure: () -> String = {
[unowned self, weak delegate = self.delegate!] in
// 这里是闭包的函数体
}

弱引用和无主引用

在闭包和捕获的实例总是互相引用并且总是同时销毁时,将闭包内的捕获定义为 无主引用

相反的,在被捕获的引用可能会变为 nil 时,将闭包内的捕获定义为 弱引用。弱引用总是可选类型,并且当引用的实例被销毁后,弱引用的值会自动置为 nil。这使我们可以在闭包体内检查它们是否存在。

注意

如果被捕获的引用绝对不会变为 nil,应该用无主引用,而不是弱引用。

前面的 HTMLElement 例子中,无主引用是正确的解决循环强引用的方法。这样编写 HTMLElement 类来避免循环强引用:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
class HTMLElement {

let name: String
let text: String?

lazy var asHTML: () -> String = {
[unowned self] in
if let text = self.text {
return "<\(self.name)>\(text)</\(self.name)>"
} else {
return "<\(self.name) />"
}
}

init(name: String, text: String? = nil) {
self.name = name
self.text = text
}

deinit {
print("\(name) is being deinitialized")
}

}

上面的 HTMLElement 实现和之前的实现一致,除了在 asHTML 闭包中多了一个捕获列表。这里,捕获列表是 [unowned self],表示“将 self 捕获为无主引用而不是强引用”。

和之前一样,我们可以创建并打印 HTMLElement 实例:

1
2
3
var paragraph: HTMLElement? = HTMLElement(name: "p", text: "hello, world")
print(paragraph!.asHTML())
// 打印“<p>hello, world</p>”

使用捕获列表后引用关系如下图所示:

这一次,闭包以无主引用的形式捕获 self,并不会持有 HTMLElement 实例的强引用。如果将 paragraph 赋值为 nilHTMLElement 实例将会被销毁,并能看到它的析构器打印出的消息:

1
2
paragraph = nil
// 打印“p is being deinitialized”

你可以查看 捕获列表 章节,获取更多关于捕获列表的信息。

留言與分享

swift泛型

分類 编程语言, swift

泛型

泛型代码让你能根据自定义的需求,编写出适用于任意类型的、灵活可复用的函数及类型。你可避免编写重复的代码,而是用一种清晰抽象的方式来表达代码的意图。

泛型是 Swift 最强大的特性之一,很多 Swift 标准库是基于泛型代码构建的。实际上,即使你没有意识到,你也一直在语言指南中使用泛型。例如,Swift 的 ArrayDictionary 都是泛型集合。你可以创建一个 Int 类型数组,也可创建一个 String 类型数组,甚至可以是任意其他 Swift 类型的数组。同样,你也可以创建一个存储任意指定类型的字典,并对该类型没有限制。

泛型解决的问题

下面是一个标准的非泛型函数 swapTwoInts(_:_:),用来交换两个 Int 值:

1
2
3
4
5
func swapTwoInts(_ a: inout Int, _ b: inout Int) {
let temporaryA = a
a = b
b = temporaryA
}

这个函数使用输入输出参数(inout)来交换 ab 的值,具体请参考 输入输出参数

swapTwoInts(_:_:) 函数将 b 的原始值换成了 a,将 a 的原始值换成了 b,你可以调用这个函数来交换两个 Int 类型变量:

1
2
3
4
5
var someInt = 3
var anotherInt = 107
swapTwoInts(&someInt, &anotherInt)
print("someInt is now \(someInt), and anotherInt is now \(anotherInt)")
// 打印“someInt is now 107, and anotherInt is now 3”

swapTwoInts(_:_:) 函数很实用,但它只能作用于 Int 类型。如果你想交换两个 String 类型值,或者 Double 类型值,你必须编写对应的函数,类似下面 swapTwoStrings(_:_:)swapTwoDoubles(_:_:) 函数:

1
2
3
4
5
6
7
8
9
10
11
func swapTwoStrings(_ a: inout String, _ b: inout String) {
let temporaryA = a
a = b
b = temporaryA
}

func swapTwoDoubles(_ a: inout Double, _ b: inout Double) {
let temporaryA = a
a = b
b = temporaryA
}

你可能注意到了,swapTwoInts(_:_:‘)swapTwoStrings(_:_:)swapTwoDoubles(_:_:) 函数体是一样的,唯一的区别是它们接受的参数类型(IntStringDouble)。

在实际应用中,通常需要一个更实用更灵活的函数来交换两个任意类型的值,幸运的是,泛型代码帮你解决了这种问题。(这些函数的泛型版本已经在下面定义好了。)

注意

在上面三个函数中,ab 类型必须相同。如果 ab 类型不同,那它们俩就不能互换值。Swift 是类型安全的语言,所以它不允许一个 String 类型的变量和一个 Double 类型的变量互换值。试图这样做将导致编译错误。

泛型函数

泛型函数可适用于任意类型,下面是函数 swapTwoInts(_:_:) 的泛型版本,命名为 swapTwoValues(_:_:)

1
2
3
4
5
func swapTwoValues<T>(_ a: inout T, _ b: inout T) {
let temporaryA = a
a = b
b = temporaryA
}

swapTwoValues(_:_:)swapTwoInts(_:_:) 函数体内容相同,它们只在第一行不同,如下所示:

1
2
func swapTwoInts(_ a: inout Int, _ b: inout Int)
func swapTwoValues<T>(_ a: inout T, _ b: inout T)

泛型版本的函数使用占位符类型名(这里叫做 T ),而不是 实际类型名(例如 IntStringDouble),占位符类型名并不关心 T 具体的类型,但它要求 ab 必须是相同的类型,T 的实际类型由每次调用 swapTwoValues(_:_:) 来决定。

泛型函数和非泛型函数的另外一个不同之处在于这个泛型函数名(swapTwoValues(_:_:))后面跟着占位类型名(T),并用尖括号括起来(<T>)。这个尖括号告诉 Swift 那个 TswapTwoValues(_:_:) 函数定义内的一个占位类型名,因此 Swift 不会去查找名为 T的实际类型。

swapTwoValues(_:_:) 函数现在可以像 swapTwoInts(_:_:) 那样调用,不同的是它能接受两个任意类型的值,条件是这两个值有着相同的类型。swapTwoValues(_:_:) 函数被调用时,T 所代表的类型都会由传入的值的类型推断出来。

在下面的两个例子中,T 分别代表 IntString

1
2
3
4
5
6
7
8
9
var someInt = 3
var anotherInt = 107
swapTwoValues(&someInt, &anotherInt)
// someInt 现在是 107,anotherInt 现在是 3

var someString = "hello"
var anotherString = "world"
swapTwoValues(&someString, &anotherString)
// someString 现在是“world”,anotherString 现在是“hello”

注意

上面定义的 swapTwoValues(_:_:) 函数是受 swap(_:_:) 函数启发而实现的。后者存在于 Swift 标准库,你可以在你的应用程序中使用它。如果你在代码中需要类似 swapTwoValues(_:_:) 函数的功能,你可以使用已存在的 swap(_:_:) 函数。

类型参数

上面 swapTwoValues(_:_:) 例子中,占位类型 T 是一个类型参数的例子,类型参数指定并命名一个占位类型,并且紧随在函数名后面,使用一对尖括号括起来(例如 <T>)。

一旦一个类型参数被指定,你可以用它来定义一个函数的参数类型(例如 swapTwoValues(_:_:) 函数中的参数 ab),或者作为函数的返回类型,还可以用作函数主体中的注释类型。在这些情况下,类型参数会在函数调用时被实际类型所替换。(在上面的 swapTwoValues(_:_:) 例子中,当函数第一次被调用时,TInt 替换,第二次调用时,被 String 替换。)

你可提供多个类型参数,将它们都写在尖括号中,用逗号分开。

命名类型参数

大多情况下,类型参数具有描述下的名称,例如字典 Dictionary<Key, Value> 中的 KeyValue 及数组 Array<Element> 中的 Element,这能告诉阅读代码的人这些参数类型与泛型类型或函数之间的关系。然而,当它们之间没有有意义的关系时,通常使用单个字符来表示,例如 TUV,例如上面演示函数 swapTwoValues(_:_:) 中的 T

注意

请始终使用大写字母开头的驼峰命名法(例如 TMyTypeParameter)来为类型参数命名,以表明它们是占位类型,而不是一个值。

泛型类型

除了泛型函数,Swift 还允许自定义泛型类型。这些自定义类、结构体和枚举可以适用于任意类型,类似于 ArrayDictionary

本节将向你展示如何编写一个名为 Stack(栈)的泛型集合类型。栈是值的有序集合,和数组类似,但比数组有更严格的操作限制。数组允许在其中任意位置插入或是删除元素。而栈只允许在集合的末端添加新的元素(称之为入栈)。类似的,栈也只能从末端移除元素(称之为出栈)。

注意

栈的概念已被 UINavigationController 类用来构造视图控制器的导航结构。你通过调用 UINavigationControllerpushViewController(_:animated:) 方法来添加新的视图控制器到导航栈,通过 popViewControllerAnimated(_:) 方法来从导航栈中移除视图控制器。每当你需要一个严格的“后进先出”方式来管理集合,栈都是最实用的模型。

下图展示了入栈(push)和出栈(pop)的行为:

  1. 现在有三个值在栈中。
  2. 第四个值被压入到栈的顶部。
  3. 现在栈中有四个值,最近入栈的那个值在顶部。
  4. 栈中最顶部的那个值被移除出栈。
  5. 一个值移除出栈后,现在栈又只有三个值了。

下面展示如何编写一个非泛型版本的栈,以 Int 型的栈为例:

1
2
3
4
5
6
7
8
9
struct IntStack {
var items = [Int]()
mutating func push(_ item: Int) {
items.append(item)
}
mutating func pop() -> Int {
return items.removeLast()
}
}

这个结构体在栈中使用一个名为 items 的数组属性来存储值。栈提供了两个方法:push(_:)pop(),用来向栈中压入值以及从栈中移除值。这些方法被标记为 mutating,因为它们需要修改结构体的 items 数组。

上面的 IntStack 结构体只能用于 Int 类型。不过,可以定义一个泛型 Stack 结构体,从而能够处理任意类型的值。

下面是相同代码的泛型版本:

1
2
3
4
5
6
7
8
9
struct Stack<Element> {
var items = [Element]()
mutating func push(_ item: Element) {
items.append(item)
}
mutating func pop() -> Element {
return items.removeLast()
}
}

注意,Stack 基本上和 IntStack 相同,只是用占位类型参数 Element 代替了实际的 Int 类型。这个类型参数包裹在紧随结构体名的一对尖括号里(<Element>)。

Element 为待提供的类型定义了一个占位名。这种待提供的类型可以在结构体的定义中通过 Element 来引用。在这个例子中,Element 在如下三个地方被用作占位符:

  • 创建 items 属性,使用 Element 类型的空数组对其进行初始化。
  • 指定 push(_:) 方法的唯一参数 item 的类型必须是 Element 类型。
  • 指定 pop() 方法的返回值类型必须是 Element 类型。

由于 Stack 是泛型类型,因此可以用来创建适用于 Swift 中任意有效类型的栈,就像 ArrayDictionary 那样。

你可以通过在尖括号中写出栈中需要存储的数据类型来创建并初始化一个 Stack 实例。例如,要创建一个 String 类型的栈,可以写成 Stack<String>()

1
2
3
4
5
6
var stackOfStrings = Stack<String>()
stackOfStrings.push("uno")
stackOfStrings.push("dos")
stackOfStrings.push("tres")
stackOfStrings.push("cuatro")
// 栈中现在有 4 个字符串

下图展示了 stackOfStrings 如何将这四个值压栈:

移除并返回栈顶部的值“cuatro”,即出栈:

1
2
let fromTheTop = stackOfStrings.pop()
// fromTheTop 的值为“cuatro”,现在栈中还有 3 个字符串

下图展示了如何将顶部的值出栈:

泛型扩展

当对泛型类型进行扩展时,你并不需要提供类型参数列表作为定义的一部分。原始类型定义中声明的类型参数列表在扩展中可以直接使用,并且这些来自原始类型中的参数名称会被用作原始定义中类型参数的引用。

下面的例子扩展了泛型类型 Stack,为其添加了一个名为 topItem 的只读计算型属性,它将会返回当前栈顶元素且不会将其从栈中移除:

1
2
3
4
5
extension Stack {
var topItem: Element? {
return items.isEmpty ? nil : items[items.count - 1]
}
}

topItem 属性会返回 Element 类型的可选值。当栈为空的时候,topItem 会返回 nil;当栈不为空的时候,topItem 会返回 items 数组中的最后一个元素。

注意:这个扩展并没有定义类型参数列表。相反的,Stack 类型已有的类型参数名称 Element,被用在扩展中来表示计算型属性 topItem 的可选类型。

计算型属性 topItem 现在可以用来访问任意 Stack 实例的顶端元素且不移除它:

1
2
3
4
if let topItem = stackOfStrings.topItem {
print("The top item on the stack is \(topItem).")
}
// 打印“The top item on the stack is tres.”

泛型类型的扩展,还可以包括类型扩展需要额外满足的条件,从而对类型添加新功能,这一部分将在具有泛型 Where 子句的扩展中进行讨论。

类型约束

swapTwoValues(_:_:) 函数和 Stack 适用于任意类型。不过,如果能对泛型函数或泛型类型中添加特定的类型约束,这将在某些情况下非常有用。类型约束指定类型参数必须继承自指定类、遵循特定的协议或协议组合。

例如,Swift 的 Dictionary 类型对字典的键的类型做了些限制。在 字典的描述 中,字典键的类型必须是可哈希(hashable)的。也就是说,必须有一种方法能够唯一地表示它。字典键之所以要是可哈希的,是为了便于检查字典中是否已经包含某个特定键的值。若没有这个要求,字典将无法判断是否可以插入或替换某个指定键的值,也不能查找到已经存储在字典中的指定键的值。

这个要求通过 Dictionary 键类型上的类型约束实现,它指明了键必须遵循 Swift 标准库中定义的 Hashable 协议。所有 Swift 的基本类型(例如 StringIntDoubleBool)默认都是可哈希的。

当自定义泛型类型时,你可以定义你自己的类型约束,这些约束将提供更为强大的泛型编程能力。像 可哈希(hashable) 这种抽象概念根据它们的概念特征来描述类型,而不是它们的具体类型。

类型约束语法

在一个类型参数名后面放置一个类名或者协议名,并用冒号进行分隔,来定义类型约束。下面将展示泛型函数约束的基本语法(与泛型类型的语法相同):

1
2
3
func someFunction<T: SomeClass, U: SomeProtocol>(someT: T, someU: U) {
// 这里是泛型函数的函数体部分
}

上面这个函数有两个类型参数。第一个类型参数 T 必须是 SomeClass 子类;第二个类型参数 U 必须符合 SomeProtocol 协议。

类型约束实践

这里有个名为 findIndex(ofString:in:) 的非泛型函数,该函数的功能是在一个 String 数组中查找给定 String 值的索引。若查找到匹配的字符串,findIndex(ofString:in:) 函数返回该字符串在数组中的索引值,否则返回 nil

1
2
3
4
5
6
7
8
func findIndex(ofString valueToFind: String, in array: [String]) -> Int? {
for (index, value) in array.enumerated() {
if value == valueToFind {
return index
}
}
return nil
}

findIndex(ofString:in:) 函数可以用于查找字符串数组中的某个字符串值:

1
2
3
4
5
let strings = ["cat", "dog", "llama", "parakeet", "terrapin"]
if let foundIndex = findIndex(ofString: "llama", in: strings) {
print("The index of llama is \(foundIndex)")
}
// 打印“The index of llama is 2”

如果只能查找字符串在数组中的索引,用处不是很大。不过,你可以用占位类型 T 替换 String 类型来写出具有相同功能的泛型函数 findIndex(_:_:)

下面展示了 findIndex(ofString:in:) 函数的泛型版本 findIndex(of:in:)。请注意这个函数返回值的类型仍然是 Int?,这是因为函数返回的是一个可选的索引数,而不是从数组中得到的一个可选值。需要提醒的是,这个函数无法通过编译,原因将在后面说明:

1
2
3
4
5
6
7
8
func findIndex<T>(of valueToFind: T, in array:[T]) -> Int? {
for (index, value) in array.enumerated() {
if value == valueToFind {
return index
}
}
return nil
}

上面所写的函数无法通过编译。问题出在相等性检查上,即 “if value == valueToFind”。不是所有的 Swift 类型都可以用等式符(==)进行比较。例如,如果你自定义类或结构体来描述复杂的数据模型,对于这个类或结构体而言,Swift 无法明确知道“相等”意味着什么。正因如此,这部分代码无法保证适用于任意类型 T,当你试图编译这部分代码时就会出现相应的错误。

不过,所有的这些并不会让我们无从下手。Swift 标准库中定义了一个 Equatable 协议,该协议要求任何遵循该协议的类型必须实现等式符(==)及不等符(!=),从而能对该类型的任意两个值进行比较。所有的 Swift 标准类型自动支持 Equatable 协议。

遵循 Equatable 协议的类型都可以安全地用于 findIndex(of:in:) 函数,因为其保证支持等式操作符。为了说明这个事情,当定义一个函数时,你可以定义一个 Equatable 类型约束作为类型参数定义的一部分:

1
2
3
4
5
6
7
8
func findIndex<T: Equatable>(of valueToFind: T, in array:[T]) -> Int? {
for (index, value) in array.enumerated() {
if value == valueToFind {
return index
}
}
return nil
}

findIndex(of:in:) 类型参数写做 T: Equatable,也就意味着“任何符合 Equatable 协议的类型 T”。

findIndex(of:in:) 函数现在可以成功编译了,并且适用于任何符合 Equatable 的类型,如 DoubleString

1
2
3
4
let doubleIndex = findIndex(of: 9.3, in: [3.14159, 0.1, 0.25])
// doubleIndex 类型为 Int?,其值为 nil,因为 9.3 不在数组中
let stringIndex = findIndex(of: "Andrea", in: ["Mike", "Malcolm", "Andrea"])
// stringIndex 类型为 Int?,其值为 2

关联类型

定义一个协议时,声明一个或多个关联类型作为协议定义的一部分将会非常有用。关联类型为协议中的某个类型提供了一个占位符名称,其代表的实际类型在协议被遵循时才会被指定。关联类型通过 associatedtype 关键字来指定。

关联类型实践

下面例子定义了一个 Container 协议,该协议定义了一个关联类型 Item

1
2
3
4
5
6
protocol Container {
associatedtype Item
mutating func append(_ item: Item)
var count: Int { get }
subscript(i: Int) -> Item { get }
}

Container 协议定义了三个任何遵循该协议的类型(即容器)必须提供的功能:

  • 必须可以通过 append(_:) 方法添加一个新元素到容器里。
  • 必须可以通过 count 属性获取容器中元素的数量,并返回一个 Int 值。
  • 必须可以通过索引值类型为 Int 的下标检索到容器中的每一个元素。

该协议没有指定容器中元素该如何存储以及元素类型。该协议只指定了任何遵从 Container 协议的类型必须提供的三个功能。遵从协议的类型在满足这三个条件的情况下,也可以提供其他额外的功能。

任何遵从 Container 协议的类型必须能够指定其存储的元素的类型。具体来说,它必须确保添加到容器内的元素以及下标返回的元素类型是正确的。

为了定义这些条件,Container 协议需要在不知道容器中元素的具体类型的情况下引用这种类型。Container 协议需要指定任何通过 append(_:) 方法添加到容器中的元素和容器内的元素是相同类型,并且通过容器下标返回的元素的类型也是这种类型。

为此,Container 协议声明了一个关联类型 Item,写作 associatedtype Item。协议没有定义 Item 是什么,这个信息留给遵从协议的类型来提供。尽管如此,Item 别名提供了一种方式来引用 Container 中元素的类型,并将之用于 append(_:) 方法和下标,从而保证任何 Container 的行为都能如预期。

这是前面非泛型版本 IntStack 类型,使其遵循 Container 协议:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
struct IntStack: Container {
// IntStack 的原始实现部分
var items = [Int]()
mutating func push(_ item: Int) {
items.append(item)
}
mutating func pop() -> Int {
return items.removeLast()
}
// Container 协议的实现部分
typealias Item = Int
mutating func append(_ item: Int) {
self.push(item)
}
var count: Int {
return items.count
}
subscript(i: Int) -> Int {
return items[i]
}
}

IntStack 结构体实现了 Container 协议的三个要求,其原有功能也不会和这些要求相冲突。

此外,IntStack 在实现 Container 的要求时,指定 ItemInt 类型,即 typealias Item = Int,从而将 Container 协议中抽象的 Item 类型转换为具体的 Int 类型。

由于 Swift 的类型推断,实际上在 IntStack 的定义中不需要声明 ItemInt。因为 IntStack 符合 Container 协议的所有要求,Swift 只需通过 append(_:) 方法的 item 参数类型和下标返回值的类型,就可以推断出 Item 的具体类型。事实上,如果你在上面的代码中删除了 typealias Item = Int 这一行,一切也可正常工作,因为 Swift 清楚地知道 Item 应该是哪种类型。

你也可以让泛型 Stack 结构体遵循 Container 协议:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
struct Stack<Element>: Container {
// Stack<Element> 的原始实现部分
var items = [Element]()
mutating func push(_ item: Element) {
items.append(item)
}
mutating func pop() -> Element {
return items.removeLast()
}
// Container 协议的实现部分
mutating func append(_ item: Element) {
self.push(item)
}
var count: Int {
return items.count
}
subscript(i: Int) -> Element {
return items[i]
}
}

这一次,占位类型参数 Element 被用作 append(_:) 方法的 item 参数和下标的返回类型。Swift 可以据此推断出 Element 的类型即是 Item 的类型。

扩展现有类型来指定关联类型

在扩展添加协议一致性 中描述了如何利用扩展让一个已存在的类型遵循一个协议,这包括使用了关联类型协议。

Swift 的 Array 类型已经提供 append(_:) 方法,count 属性,以及带有 Int 索引的下标来检索其元素。这三个功能都符合 Container 协议的要求,也就意味着你只需声明 Array 遵循Container 协议,就可以扩展 Array,使其遵从 Container 协议。你可以通过一个空扩展来实现这点,正如通过扩展采纳协议中的描述:

1
extension Array: Container {}

Arrayappend(_:) 方法和下标确保了 Swift 可以推断出 Item 具体类型。定义了这个扩展后,你可以将任意 Array 当作 Container 来使用。

给关联类型添加约束

你可以在协议里给关联类型添加约束来要求遵循的类型满足约束。例如,下面的代码定义了 Container 协议, 要求关联类型 Item 必须遵循 Equatable 协议:

1
2
3
4
5
6
protocol Container {
associatedtype Item: Equatable
mutating func append(_ item: Item)
var count: Int { get }
subscript(i: Int) -> Item { get }
}

要遵守 Container 协议,Item 类型也必须遵守 Equatable 协议。

在关联类型约束里使用协议

协议可以作为它自身的要求出现。例如,有一个协议细化了 Container 协议,添加了一个suffix(_:) 方法。suffix(_:) 方法返回容器中从后往前给定数量的元素,并把它们存储在一个 Suffix 类型的实例里。

1
2
3
4
protocol SuffixableContainer: Container {
associatedtype Suffix: SuffixableContainer where Suffix.Item == Item
func suffix(_ size: Int) -> Suffix
}

在这个协议里,Suffix 是一个关联类型,就像上边例子中 ContainerItem 类型一样。Suffix 拥有两个约束:它必须遵循 SuffixableContainer 协议(就是当前定义的协议),以及它的 Item 类型必须是和容器里的 Item 类型相同。Item 的约束是一个 where 分句,它在下面具有泛型 Where 子句的扩展中有讨论。

这是上面 泛型类型Stack 类型的扩展,它遵循了 SuffixableContainer 协议:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
extension Stack: SuffixableContainer {
func suffix(_ size: Int) -> Stack {
var result = Stack()
for index in (count-size)..<count {
result.append(self[index])
}
return result
}
// 推断 suffix 结果是Stack。
}
var stackOfInts = Stack<Int>()
stackOfInts.append(10)
stackOfInts.append(20)
stackOfInts.append(30)
let suffix = stackOfInts.suffix(2)
// suffix 包含 20 和 30

在上面的例子中,SuffixStack 的关联类型,也是 Stack ,所以 Stack 的后缀运算返回另一个 Stack 。另外,遵循 SuffixableContainer 的类型可以拥有一个与它自己不同的 Suffix 类型——也就是说后缀运算可以返回不同的类型。比如说,这里有一个非泛型 IntStack 类型的扩展,它遵循了 SuffixableContainer 协议,使用 Stack<Int> 作为它的后缀类型而不是 IntStack

1
2
3
4
5
6
7
8
9
10
extension IntStack: SuffixableContainer {
func suffix(_ size: Int) -> Stack<Int> {
var result = Stack<Int>()
for index in (count-size)..<count {
result.append(self[index])
}
return result
}
// 推断 suffix 结果是 Stack<Int>。
}

泛型 Where 语句

类型约束 让你能够为泛型函数、下标、类型的类型参数定义一些强制要求。

对关联类型添加约束通常是非常有用的。你可以通过定义一个泛型 where 子句来实现。通过泛型 where 子句让关联类型遵从某个特定的协议,以及某个特定的类型参数和关联类型必须类型相同。你可以通过将 where 关键字紧跟在类型参数列表后面来定义 where 子句,where 子句后跟一个或者多个针对关联类型的约束,以及一个或多个类型参数和关联类型间的相等关系。你可以在函数体或者类型的大括号之前添加 where 子句。

下面的例子定义了一个名为 allItemsMatch 的泛型函数,用来检查两个 Container 实例是否包含相同顺序的相同元素。如果所有的元素能够匹配,那么返回 true,否则返回 false

被检查的两个 Container 可以不是相同类型的容器(虽然它们可以相同),但它们必须拥有相同类型的元素。这个要求通过一个类型约束以及一个 where 子句来表示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
func allItemsMatch<C1: Container, C2: Container>
(_ someContainer: C1, _ anotherContainer: C2) -> Bool
where C1.Item == C2.Item, C1.Item: Equatable {

// 检查两个容器含有相同数量的元素
if someContainer.count != anotherContainer.count {
return false
}

// 检查每一对元素是否相等
for i in 0..<someContainer.count {
if someContainer[i] != anotherContainer[i] {
return false
}
}

// 所有元素都匹配,返回 true
return true
}

这个函数接受 someContaineranotherContainer 两个参数。参数 someContainer 的类型为 C1,参数 anotherContainer 的类型为 C2C1C2 是容器的两个占位类型参数,函数被调用时才能确定它们的具体类型。

这个函数的类型参数列表还定义了对两个类型参数的要求:

  • C1 必须符合 Container 协议(写作 C1: Container)。
  • C2 必须符合 Container 协议(写作 C2: Container)。
  • C1Item 必须和 C2Item 类型相同(写作 C1.Item == C2.Item)。
  • C1Item 必须符合 Equatable 协议(写作 C1.Item: Equatable)。

前两个要求定义在函数的类型形式参数列表里,后两个要求定义在了函数的泛型 where 分句中。

这些要求意味着:

  • someContainer 是一个 C1 类型的容器。
  • anotherContainer 是一个 C2 类型的容器。
  • someContaineranotherContainer 包含相同类型的元素。
  • someContainer 中的元素可以通过不等于操作符(!=)来检查它们是否相同。

第三个和第四个要求结合起来意味着 anotherContainer 中的元素也可以通过 != 操作符来比较,因为它们和 someContainer 中的元素类型相同。

这些要求让 allItemsMatch(_:_:) 函数能够比较两个容器,即使它们的容器类型不同。

allItemsMatch(_:_:) 函数首先检查两个容器元素个数是否相同,如果元素个数不同,那么一定不匹配,函数就会返回 false

进行这项检查之后,通过 for-in 循环和半闭区间操作符(..<)来迭代每个元素,检查 someContainer 中的元素是否不等于 anotherContainer 中的对应元素。如果两个元素不相等,那么两个容器不匹配,函数返回 false。

如果循环体结束后未发现任何不匹配的情况,表明两个容器匹配,函数返回 true

下面是 allItemsMatch(_:_:) 函数的示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
var stackOfStrings = Stack<String>()
stackOfStrings.push("uno")
stackOfStrings.push("dos")
stackOfStrings.push("tres")

var arrayOfStrings = ["uno", "dos", "tres"]

if allItemsMatch(stackOfStrings, arrayOfStrings) {
print("All items match.")
} else {
print("Not all items match.")
}
// 打印“All items match.”

上面的例子创建 Stack 实例来存储 String 值,然后将三个字符串压栈。这个例子还通过数组字面量创建了一个 Array 实例,数组中包含同栈中一样的三个字符串。即使栈和数组是不同的类型,但它们都遵从 Container 协议,而且它们都包含相同类型的值。因此你可以用这两个容器作为参数来调用 allItemsMatch(_:_:) 函数。在上面的例子中,allItemsMatch(_:_:) 函数正确地显示了这两个容器中的所有元素都是相互匹配的。

具有泛型 Where 子句的扩展

你也可以使用泛型 where 子句作为扩展的一部分。基于以前的例子,下面的示例扩展了泛型 Stack 结构体,添加一个 isTop(_:) 方法。

1
2
3
4
5
6
7
8
extension Stack where Element: Equatable {
func isTop(_ item: Element) -> Bool {
guard let topItem = items.last else {
return false
}
return topItem == item
}
}

这个新的 isTop(_:) 方法首先检查这个栈是不是空的,然后比较给定的元素与栈顶部的元素。如果你尝试不用泛型 where 子句,会有一个问题:在 isTop(_:) 里面使用了 == 运算符,但是 Stack 的定义没有要求它的元素是符合 Equatable 协议的,所以使用 == 运算符导致编译时错误。使用泛型 where 子句可以为扩展添加新的条件,因此只有当栈中的元素符合 Equatable 协议时,扩展才会添加 isTop(_:) 方法。

以下是 isTop(_:) 方法的调用方式:

1
2
3
4
5
6
if stackOfStrings.isTop("tres") {
print("Top element is tres.")
} else {
print("Top element is something else.")
}
// 打印“Top element is tres.”

如果尝试在其元素不符合 Equatable 协议的栈上调用 isTop(_:) 方法,则会收到编译时错误。

1
2
3
4
5
struct NotEquatable { }
var notEquatableStack = Stack<NotEquatable>()
let notEquatableValue = NotEquatable()
notEquatableStack.push(notEquatableValue)
notEquatableStack.isTop(notEquatableValue) // 报错

你可以使用泛型 where 子句去扩展一个协议。基于以前的示例,下面的示例扩展了 Container 协议,添加一个 startsWith(_:) 方法。

1
2
3
4
5
extension Container where Item: Equatable {
func startsWith(_ item: Item) -> Bool {
return count >= 1 && self[0] == item
}
}

这个 startsWith(_:) 方法首先确保容器至少有一个元素,然后检查容器中的第一个元素是否与给定的元素相等。任何符合 Container 协议的类型都可以使用这个新的 startsWith(_:) 方法,包括上面使用的栈和数组,只要容器的元素是符合 Equatable 协议的。

1
2
3
4
5
6
if [9, 9, 9].startsWith(42) {
print("Starts with 42.")
} else {
print("Starts with something else.")
}
// 打印“Starts with something else.”

上述示例中的泛型 where 子句要求 Item 遵循协议,但也可以编写一个泛型 where 子句去要求 Item 为特定类型。例如:

1
2
3
4
5
6
7
8
9
10
11
extension Container where Item == Double {
func average() -> Double {
var sum = 0.0
for index in 0..<count {
sum += self[index]
}
return sum / Double(count)
}
}
print([1260.0, 1200.0, 98.6, 37.0].average())
// 打印“648.9”

此示例将一个 average() 方法添加到 Item 类型为 Double 的容器中。此方法遍历容器中的元素将其累加,并除以容器的数量计算平均值。它将数量从 Int 转换为 Double 确保能够进行浮点除法。

就像可以在其他地方写泛型 where 子句一样,你可以在一个泛型 where 子句中包含多个条件作为扩展的一部分。用逗号分隔列表中的每个条件。

具有泛型 Where 子句的关联类型

你可以在关联类型后面加上具有泛型 where 的字句。例如,建立一个包含迭代器(Iterator)的容器,就像是标准库中使用的 Sequence 协议那样。你应该这么写:

1
2
3
4
5
6
7
8
9
protocol Container {
associatedtype Item
mutating func append(_ item: Item)
var count: Int { get }
subscript(i: Int) -> Item { get }

associatedtype Iterator: IteratorProtocol where Iterator.Element == Item
func makeIterator() -> Iterator
}

迭代器(Iterator)的泛型 where 子句要求:无论迭代器是什么类型,迭代器中的元素类型,必须和容器项目的类型保持一致。makeIterator() 则提供了容器的迭代器的访问接口。

一个协议继承了另一个协议,你通过在协议声明的时候,包含泛型 where 子句,来添加了一个约束到被继承协议的关联类型。例如,下面的代码声明了一个 ComparableContainer 协议,它要求所有的 Item 必须是 Comparable 的。

1
protocol ComparableContainer: Container where Item: Comparable { }

泛型下标

下标可以是泛型,它们能够包含泛型 where 子句。你可以在 subscript 后用尖括号来写占位符类型,你还可以在下标代码块花括号前写 where 子句。例如:

1
2
3
4
5
6
7
8
9
10
extension Container {
subscript<Indices: Sequence>(indices: Indices) -> [Item]
where Indices.Iterator.Element == Int {
var result = [Item]()
for index in indices {
result.append(self[index])
}
return result
}
}

这个 Container 协议的扩展添加了一个下标方法,接收一个索引的集合,返回每一个索引所在的值的数组。这个泛型下标的约束如下:

  • 在尖括号中的泛型参数 Indices,必须是符合标准库中的 Sequence 协议的类型。
  • 下标使用的单一的参数,indices,必须是 Indices 的实例。
  • 泛型 where 子句要求 Sequence(Indices)的迭代器,其所有的元素都是 Int 类型。这样就能确保在序列(Sequence)中的索引和容器(Container)里面的索引类型是一致的。

综合一下,这些约束意味着,传入到 indices 下标,是一个整型的序列。

留言與分享

swift协议

分類 编程语言, swift

协议

协议 定义了一个蓝图,规定了用来实现某一特定任务或者功能的方法、属性,以及其他需要的东西。类、结构体或枚举都可以遵循协议,并为协议定义的这些要求提供具体实现。某个类型能够满足某个协议的要求,就可以说该类型遵循这个协议。

除了遵循协议的类型必须实现的要求外,还可以对协议进行扩展,通过扩展来实现一部分要求或者实现一些附加功能,这样遵循协议的类型就能够使用这些功能。

协议语法

协议的定义方式与类、结构体和枚举的定义非常相似:

1
2
3
protocol SomeProtocol {
// 这里是协议的定义部分
}

要让自定义类型遵循某个协议,在定义类型时,需要在类型名称后加上协议名称,中间以冒号(:)分隔。遵循多个协议时,各协议之间用逗号(,)分隔:

1
2
3
struct SomeStructure: FirstProtocol, AnotherProtocol {
// 这里是结构体的定义部分
}

若一个拥有父类的类在遵循协议时,应该将父类名放在协议名之前,以逗号分隔:

1
2
3
class SomeClass: SomeSuperClass, FirstProtocol, AnotherProtocol {
// 这里是类的定义部分
}

属性要求

协议可以要求遵循协议的类型提供特定名称和类型的实例属性或类型属性。协议不指定属性是存储属性还是计算属性,它只指定属性的名称和类型。此外,协议还指定属性是可读的还是可读可写的

如果协议要求属性是可读可写的,那么该属性不能是常量属性或只读的计算型属性。如果协议只要求属性是可读的,那么该属性不仅可以是可读的,如果代码需要的话,还可以是可写的。

协议总是用 var 关键字来声明变量属性,在类型声明后加上 { set get } 来表示属性是可读可写的,可读属性则用 { get } 来表示:

1
2
3
4
protocol SomeProtocol {
var mustBeSettable: Int { get set }
var doesNotNeedToBeSettable: Int { get }
}

在协议中定义类型属性时,总是使用 static 关键字作为前缀。当类类型遵循协议时,除了 static 关键字,还可以使用 class 关键字来声明类型属性:

1
2
3
protocol AnotherProtocol {
static var someTypeProperty: Int { get set }
}

如下所示,这是一个只含有一个实例属性要求的协议:

1
2
3
protocol FullyNamed {
var fullName: String { get }
}

FullyNamed 协议除了要求遵循协议的类型提供 fullName 属性外,并没有其他特别的要求。这个协议表示,任何遵循 FullyNamed 的类型,都必须有一个可读的 String 类型的实例属性 fullName

下面是一个遵循 FullyNamed 协议的简单结构体:

1
2
3
4
5
struct Person: FullyNamed {
var fullName: String
}
let john = Person(fullName: "John Appleseed")
// john.fullName 为 "John Appleseed"

这个例子中定义了一个叫做 Person 的结构体,用来表示一个具有名字的人。从第一行代码可以看出,它遵循了 FullyNamed 协议。

Person 结构体的每一个实例都有一个 String 类型的存储型属性 fullName。这正好满足了 FullyNamed 协议的要求,也就意味着 Person 结构体正确地符合了协议。(如果协议要求未被完全满足,在编译时会报错。)

下面是一个更为复杂的类,它采纳并遵循了 FullyNamed 协议:

1
2
3
4
5
6
7
8
9
10
11
12
13
class Starship: FullyNamed {
var prefix: String?
var name: String
init(name: String, prefix: String? = nil) {
self.name = name
self.prefix = prefix
}
var fullName: String {
return (prefix != nil ? prefix! + " " : "") + name
}
}
var ncc1701 = Starship(name: "Enterprise", prefix: "USS")
// ncc1701.fullName 为 "USS Enterprise"

Starship 类把 fullName 作为只读的计算属性来实现。每一个 Starship 类的实例都有一个名为 name 的非可选属性和一个名为 prefix 的可选属性。 当 prefix 存在时,计算属性 fullName 会将 prefix 插入到 name 之前,从而得到一个带有 prefixfullName

方法要求

协议可以要求遵循协议的类型实现某些指定的实例方法或类方法。这些方法作为协议的一部分,像普通方法一样放在协议的定义中,但是不需要大括号和方法体。可以在协议中定义具有可变参数的方法,和普通方法的定义方式相同。但是,不支持为协议中的方法提供默认参数。

正如属性要求中所述,在协议中定义类方法的时候,总是使用 static 关键字作为前缀。即使在类实现时,类方法要求使用 classstatic 作为关键字前缀,前面的规则仍然适用:

1
2
3
protocol SomeProtocol {
static func someTypeMethod()
}

下面的例子定义了一个只含有一个实例方法的协议:

1
2
3
protocol RandomNumberGenerator {
func random() -> Double
}

RandomNumberGenerator 协议要求遵循协议的类型必须拥有一个名为 random, 返回值类型为 Double 的实例方法。尽管这里并未指明,但是我们假设返回值是从 0.0 到(但不包括)1.0

RandomNumberGenerator 协议并不关心每一个随机数是怎样生成的,它只要求必须提供一个随机数生成器。

如下所示,下边是一个遵循并符合 RandomNumberGenerator 协议的类。该类实现了一个叫做 线性同余生成器(linear congruential generator) 的伪随机数算法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class LinearCongruentialGenerator: RandomNumberGenerator {
var lastRandom = 42.0
let m = 139968.0
let a = 3877.0
let c = 29573.0
func random() -> Double {
lastRandom = ((lastRandom * a + c).truncatingRemainder(dividingBy:m))
return lastRandom / m
}
}
let generator = LinearCongruentialGenerator()
print("Here's a random number: \(generator.random())")
// 打印 “Here's a random number: 0.37464991998171”
print("And another one: \(generator.random())")
// 打印 “And another one: 0.729023776863283”

异变方法要求

有时需要在方法中改变(或异变)方法所属的实例。例如,在值类型(即结构体和枚举)的实例方法中,将 mutating 关键字作为方法的前缀,写在 func 关键字之前,表示可以在该方法中修改它所属的实例以及实例的任意属性的值。这一过程在 在实例方法中修改值类型 章节中有详细描述。

如果你在协议中定义了一个实例方法,该方法会改变遵循该协议的类型的实例,那么在定义协议时需要在方法前加 mutating 关键字。这使得结构体和枚举能够遵循此协议并满足此方法要求。

注意

实现协议中的 mutating 方法时,若是类类型,则不用写 mutating 关键字。而对于结构体和枚举,则必须写 mutating 关键字。

如下所示,Togglable 协议只定义了一个名为 toggle 的实例方法。顾名思义,toggle() 方法将改变实例属性,从而切换遵循该协议类型的实例的状态。

toggle() 方法在定义的时候,使用 mutating 关键字标记,这表明当它被调用时,该方法将会改变遵循协议的类型的实例:

1
2
3
protocol Togglable {
mutating func toggle()
}

当使用枚举或结构体来实现 Togglable 协议时,需要提供一个带有 mutating 前缀的 toggle() 方法。

下面定义了一个名为 OnOffSwitch 的枚举。这个枚举在两种状态之间进行切换,用枚举成员 OnOff 表示。枚举的 toggle() 方法被标记为 mutating,以满足 Togglable 协议的要求:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
enum OnOffSwitch: Togglable {
case off, on
mutating func toggle() {
switch self {
case .off:
self = .on
case .on:
self = .off
}
}
}
var lightSwitch = OnOffSwitch.off
lightSwitch.toggle()
// lightSwitch 现在的值为 .on

构造器要求

协议可以要求遵循协议的类型实现指定的构造器。你可以像编写普通构造器那样,在协议的定义里写下构造器的声明,但不需要写花括号和构造器的实体:

1
2
3
protocol SomeProtocol {
init(someParameter: Int)
}

协议构造器要求的类实现

你可以在遵循协议的类中实现构造器,无论是作为指定构造器,还是作为便利构造器。无论哪种情况,你都必须为构造器实现标上 required 修饰符:

1
2
3
4
5
class SomeClass: SomeProtocol {
required init(someParameter: Int) {
// 这里是构造器的实现部分
}
}

使用 required 修饰符可以确保所有子类也必须提供此构造器实现,从而也能符合协议。

关于 required 构造器的更多内容,请参考 必要构造器

注意

如果类已经被标记为 final,那么不需要在协议构造器的实现中使用 required 修饰符,因为 final 类不能有子类。关于 final 修饰符的更多内容,请参见 防止重写

如果一个子类重写了父类的指定构造器,并且该构造器满足了某个协议的要求,那么该构造器的实现需要同时标注 requiredoverride 修饰符:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
protocol SomeProtocol {
init()
}

class SomeSuperClass {
init() {
// 这里是构造器的实现部分
}
}

class SomeSubClass: SomeSuperClass, SomeProtocol {
// 因为遵循协议,需要加上 required
// 因为继承自父类,需要加上 override
required override init() {
// 这里是构造器的实现部分
}
}

可失败构造器要求

协议还可以为遵循协议的类型定义可失败构造器要求,详见 可失败构造器

遵循协议的类型可以通过可失败构造器(init?)或非可失败构造器(init)来满足协议中定义的可失败构造器要求。协议中定义的非可失败构造器要求可以通过非可失败构造器(init)或隐式解包可失败构造器(init!)来满足。

协议作为类型

尽管协议本身并未实现任何功能,但是协议可以被当做一个功能完备的类型来使用。协议作为类型使用,有时被称作「存在类型」,这个名词来自「存在着一个类型 T,该类型遵循协议 T」。

协议可以像其他普通类型一样使用,使用场景如下:

  • 作为函数、方法或构造器中的参数类型或返回值类型
  • 作为常量、变量或属性的类型
  • 作为数组、字典或其他容器中的元素类型

注意

协议是一种类型,因此协议类型的名称应与其他类型(例如 IntDoubleString)的写法相同,使用大写字母开头的驼峰式写法,例如(FullyNamedRandomNumberGenerator)。

下面是将协议作为类型使用的例子:

1
2
3
4
5
6
7
8
9
10
11
class Dice {
let sides: Int
let generator: RandomNumberGenerator
init(sides: Int, generator: RandomNumberGenerator) {
self.sides = sides
self.generator = generator
}
func roll() -> Int {
return Int(generator.random() * Double(sides)) + 1
}
}

例子中定义了一个 Dice 类,用来代表桌游中拥有 N 个面的骰子。Dice 的实例含有 sidesgenerator 两个属性,前者是整型,用来表示骰子有几个面,后者为骰子提供一个随机数生成器,从而生成随机点数。

generator 属性的类型为 RandomNumberGenerator,因此任何遵循了 RandomNumberGenerator 协议的类型的实例都可以赋值给 generator,除此之外并无其他要求。并且由于其类型是 RandomNumberGenerator,在 Dice 类中与 generator 交互的代码,必须适用于所有 generator 实例都遵循的方法。这句话的意思是不能使用由 generator 底层类型提供的任何方法或属性。但是你可以通过向下转型,从协议类型转换成底层实现类型,比如从父类向下转型为子类。请参考 向下转型

Dice 类还有一个构造器,用来设置初始状态。构造器有一个名为 generator,类型为 RandomNumberGenerator 的形参。在调用构造方法创建 Dice 的实例时,可以传入任何遵循 RandomNumberGenerator 协议的实例给 generator

Dice 类提供了一个名为 roll 的实例方法,用来模拟骰子的面值。它先调用 generatorrandom() 方法来生成一个 [0.0,1.0) 区间内的随机数,然后使用这个随机数生成正确的骰子面值。因为 generator 遵循了 RandomNumberGenerator 协议,可以确保它有个 random() 方法可供调用。

下面的例子展示了如何使用 LinearCongruentialGenerator 的实例作为随机数生成器来创建一个六面骰子:

1
2
3
4
5
6
7
8
9
var d6 = Dice(sides: 6, generator: LinearCongruentialGenerator())
for _ in 1...5 {
print("Random dice roll is \(d6.roll())")
}
// Random dice roll is 3
// Random dice roll is 5
// Random dice roll is 4
// Random dice roll is 5
// Random dice roll is 4

委托

委托是一种设计模式,它允许类或结构体将一些需要它们负责的功能委托给其他类型的实例。委托模式的实现很简单:定义协议来封装那些需要被委托的功能,这样就能确保遵循协议的类型能提供这些功能。委托模式可以用来响应特定的动作,或者接收外部数据源提供的数据,而无需关心外部数据源的类型。

下面的例子定义了两个基于骰子游戏的协议:

1
2
3
4
5
6
7
8
9
protocol DiceGame {
var dice: Dice { get }
func play()
}
protocol DiceGameDelegate {
func gameDidStart(_ game: DiceGame)
func game(_ game: DiceGame, didStartNewTurnWithDiceRoll diceRoll: Int)
func gameDidEnd(_ game: DiceGame)
}

DiceGame 协议可以被任意涉及骰子的游戏遵循。

DiceGameDelegate 协议可以被任意类型遵循,用来追踪 DiceGame 的游戏过程。为了防止强引用导致的循环引用问题,可以把协议声明为弱引用,更多相关的知识请看 类实例之间的循环强引用,当协议标记为类专属可以使 SnakesAndLadders 类在声明协议时强制要使用弱引用。若要声明类专属的协议就必须继承于 AnyObject ,更多请看 类专属的协议

如下所示,SnakesAndLadders控制流 章节引入的蛇梯棋游戏的新版本。新版本使用 Dice 实例作为骰子,并且实现了 DiceGameDiceGameDelegate 协议,后者用来记录游戏的过程:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
class SnakesAndLadders: DiceGame {
let finalSquare = 25
let dice = Dice(sides: 6, generator: LinearCongruentialGenerator())
var square = 0
var board: [Int]
init() {
board = Array(repeating: 0, count: finalSquare + 1)
board[03] = +08; board[06] = +11; board[09] = +09; board[10] = +02
board[14] = -10; board[19] = -11; board[22] = -02; board[24] = -08
}
var delegate: DiceGameDelegate?
func play() {
square = 0
delegate?.gameDidStart(self)
gameLoop: while square != finalSquare {
let diceRoll = dice.roll()
delegate?.game(self, didStartNewTurnWithDiceRoll: diceRoll)
switch square + diceRoll {
case finalSquare:
break gameLoop
case let newSquare where newSquare > finalSquare:
continue gameLoop
default:
square += diceRoll
square += board[square]
}
}
delegate?.gameDidEnd(self)
}
}

关于这个蛇梯棋游戏的详细描述请参阅 中断(Break)

这个版本的游戏封装到了 SnakesAndLadders 类中,该类遵循了 DiceGame 协议,并且提供了相应的可读的 dice 属性和 play() 方法。( dice 属性在构造之后就不再改变,且协议只要求 dice 为可读的,因此将 dice 声明为常量属性。)

游戏使用 SnakesAndLadders 类的 init() 构造器来初始化游戏。所有的游戏逻辑被转移到了协议中的 play() 方法,play() 方法使用协议要求的 dice 属性提供骰子摇出的值。

注意,delegate 并不是游戏的必备条件,因此 delegate 被定义为 DiceGameDelegate 类型的可选属性。因为 delegate 是可选值,因此会被自动赋予初始值 nil。随后,可以在游戏中为 delegate 设置适当的值。

DicegameDelegate 协议提供了三个方法用来追踪游戏过程。这三个方法被放置于游戏的逻辑中,即 play() 方法内。分别在游戏开始时,新一轮开始时,以及游戏结束时被调用。

因为 delegate 是一个 DiceGameDelegate 类型的可选属性,因此在 play() 方法中通过可选链式调用来调用它的方法。若 delegate 属性为 nil,则调用方法会优雅地失败,并不会产生错误。若 delegate 不为 nil,则方法能够被调用,并传递 SnakesAndLadders 实例作为参数。

如下示例定义了 DiceGameTracker 类,它遵循了 DiceGameDelegate 协议:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class DiceGameTracker: DiceGameDelegate {
var numberOfTurns = 0
func gameDidStart(_ game: DiceGame) {
numberOfTurns = 0
if game is SnakesAndLadders {
print("Started a new game of Snakes and Ladders")
}
print("The game is using a \(game.dice.sides)-sided dice")
}
func game(_ game: DiceGame, didStartNewTurnWithDiceRoll diceRoll: Int) {
numberOfTurns += 1
print("Rolled a \(diceRoll)")
}
func gameDidEnd(_ game: DiceGame) {
print("The game lasted for \(numberOfTurns) turns")
}
}

DiceGameTracker 实现了 DiceGameDelegate 协议要求的三个方法,用来记录游戏已经进行的轮数。当游戏开始时,numberOfTurns 属性被赋值为 0,然后在每新一轮中递增,游戏结束后,打印游戏的总轮数。

gameDidStart(_:) 方法从 game 参数获取游戏信息并打印。game 参数是 DiceGame 类型而不是 SnakeAndLadders 类型,所以在 gameDidStart(_:) 方法中只能访问 DiceGame 协议中的内容。当然了,SnakeAndLadders 的方法也可以在类型转换之后调用。在上例代码中,通过 is 操作符检查 game 是否为 SnakesAndLadders 类型的实例,如果是,则打印出相应的消息。

无论当前进行的是何种游戏,由于 game 符合 DiceGame 协议,可以确保 game 含有 dice 属性。因此在 gameDidStart(_:) 方法中可以通过传入的 game 参数来访问 dice 属性,进而打印出 dicesides 属性的值。

DiceGameTracker 的运行情况如下所示:

1
2
3
4
5
6
7
8
9
10
11
let tracker = DiceGameTracker()
let game = SnakesAndLadders()
game.delegate = tracker
game.play()
// Started a new game of Snakes and Ladders
// The game is using a 6-sided dice
// Rolled a 3
// Rolled a 5
// Rolled a 4
// Rolled a 5
// The game lasted for 4 turns

在扩展里添加协议遵循

即便无法修改源代码,依然可以通过扩展令已有类型遵循并符合协议。扩展可以为已有类型添加属性、方法、下标以及构造器,因此可以符合协议中的相应要求。详情请在 扩展 章节中查看。

注意

通过扩展令已有类型遵循并符合协议时,该类型的所有实例也会随之获得协议中定义的各项功能。

例如下面这个 TextRepresentable 协议,任何想要通过文本表示一些内容的类型都可以实现该协议。这些想要表示的内容可以是实例本身的描述,也可以是实例当前状态的文本描述:

1
2
3
protocol TextRepresentable {
var textualDescription: String { get }
}

可以通过扩展,令先前提到的 Dice 类可以扩展来采纳和遵循 TextRepresentable 协议:

1
2
3
4
5
extension Dice: TextRepresentable {
var textualDescription: String {
return "A \(sides)-sided dice"
}
}

通过扩展遵循并采纳协议,和在原始定义中遵循并符合协议的效果完全相同。协议名称写在类型名之后,以冒号隔开,然后在扩展的大括号内实现协议要求的内容。

现在所有 Dice 的实例都可以看做 TextRepresentable 类型:

1
2
3
let d12 = Dice(sides: 12, generator: LinearCongruentialGenerator())
print(d12.textualDescription)
// 打印 “A 12-sided dice”

同样,SnakesAndLadders 类也可以通过扩展来采纳和遵循 TextRepresentable 协议:

1
2
3
4
5
6
7
extension SnakesAndLadders: TextRepresentable {
var textualDescription: String {
return "A game of Snakes and Ladders with \(finalSquare) squares"
}
}
print(game.textualDescription)
// 打印 “A game of Snakes and Ladders with 25 squares”

有条件地遵循协议

泛型类型可能只在某些情况下满足一个协议的要求,比如当类型的泛型形式参数遵循对应协议时。你可以通过在扩展类型时列出限制让泛型类型有条件地遵循某协议。在你采纳协议的名字后面写泛型 where 分句。更多关于泛型 where 分句,见 泛型 Where 分句

下面的扩展让 Array 类型只要在存储遵循 TextRepresentable 协议的元素时就遵循 TextRepresentable 协议。

1
2
3
4
5
6
7
8
9
extension Array: TextRepresentable where Element: TextRepresentable {
var textualDescription: String {
let itemsAsText = self.map { $0.textualDescription }
return "[" + itemsAsText.joined(separator: ", ") + "]"
}
}
let myDice = [d6, d12]
print(myDice.textualDescription)
// 打印 "[A 6-sided dice, A 12-sided dice]"

在扩展里声明采纳协议

当一个类型已经符合了某个协议中的所有要求,却还没有声明采纳该协议时,可以通过空的扩展来让它采纳该协议:

1
2
3
4
5
6
7
struct Hamster {
var name: String
var textualDescription: String {
return "A hamster named \(name)"
}
}
extension Hamster: TextRepresentable {}

从现在起,Hamster 的实例可以作为 TextRepresentable 类型使用:

1
2
3
4
let simonTheHamster = Hamster(name: "Simon")
let somethingTextRepresentable: TextRepresentable = simonTheHamster
print(somethingTextRepresentable.textualDescription)
// 打印 “A hamster named Simon”

注意

即使满足了协议的所有要求,类型也不会自动遵循协议,必须显式地遵循协议。

协议类型的集合

协议类型可以在数组或者字典这样的集合中使用,在 协议类型 提到了这样的用法。下面的例子创建了一个元素类型为 TextRepresentable 的数组:

1
let things: [TextRepresentable] = [game, d12, simonTheHamster]

如下所示,可以遍历 things 数组,并打印每个元素的文本表示:

1
2
3
4
5
6
for thing in things {
print(thing.textualDescription)
}
// A game of Snakes and Ladders with 25 squares
// A 12-sided dice
// A hamster named Simon

注意 thing 常量是 TextRepresentable 类型而不是 DiceDiceGameHamster 等类型,即使实例在幕后确实是这些类型中的一种。由于 thingTextRepresentable 类型,任何 TextRepresentable 的实例都有一个 textualDescription 属性,所以在每次循环中可以安全地访问 thing.textualDescription

协议的继承

协议能够继承一个或多个其他协议,可以在继承的协议的基础上增加新的要求。协议的继承语法与类的继承相似,多个被继承的协议间用逗号分隔:

1
2
3
protocol InheritingProtocol: SomeProtocol, AnotherProtocol {
// 这里是协议的定义部分
}

如下所示,PrettyTextRepresentable 协议继承了 TextRepresentable 协议:

1
2
3
protocol PrettyTextRepresentable: TextRepresentable {
var prettyTextualDescription: String { get }
}

例子中定义了一个新的协议 PrettyTextRepresentable,它继承自 TextRepresentable 协议。任何遵循 PrettyTextRepresentable 协议的类型在满足该协议的要求时,也必须满足 TextRepresentable 协议的要求。在这个例子中,PrettyTextRepresentable 协议额外要求遵循协议的类型提供一个返回值为 String 类型的 prettyTextualDescription 属性。

如下所示,扩展 SnakesAndLadders,使其遵循并符合 PrettyTextRepresentable 协议:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
extension SnakesAndLadders: PrettyTextRepresentable {
var prettyTextualDescription: String {
var output = textualDescription + ":\n"
for index in 1...finalSquare {
switch board[index] {
case let ladder where ladder > 0:
output += "▲ "
case let snake where snake < 0:
output += "▼ "
default:
output += "○ "
}
}
return output
}
}

上述扩展令 SnakesAndLadders 遵循了 PrettyTextRepresentable 协议,并提供了协议要求的 prettyTextualDescription 属性。每个 PrettyTextRepresentable 类型同时也是 TextRepresentable 类型,所以在 prettyTextualDescription 的实现中,可以访问 textualDescription 属性。然后,拼接上了冒号和换行符。接着,遍历数组中的元素,拼接一个几何图形来表示每个棋盘方格的内容:

  • 当从数组中取出的元素的值大于 0 时,用 表示。
  • 当从数组中取出的元素的值小于 0 时,用 表示。
  • 当从数组中取出的元素的值等于 0 时,用 表示。

任意 SankesAndLadders 的实例都可以使用 prettyTextualDescription 属性来打印一个漂亮的文本描述:

1
2
3
print(game.prettyTextualDescription)
// A game of Snakes and Ladders with 25 squares:
// ○ ○ ▲ ○ ○ ▲ ○ ○ ▲ ▲ ○ ○ ○ ▼ ○ ○ ○ ○ ▼ ○ ○ ▼ ○ ▼ ○

类专属的协议

你通过添加 AnyObject 关键字到协议的继承列表,就可以限制协议只能被类类型采纳(以及非结构体或者非枚举的类型)。

1
2
3
protocol SomeClassOnlyProtocol: AnyObject, SomeInheritedProtocol {
// 这里是类专属协议的定义部分
}

在以上例子中,协议 SomeClassOnlyProtocol 只能被类类型采纳。如果尝试让结构体或枚举类型采纳 SomeClassOnlyProtocol,则会导致编译时错误。

注意

当协议定义的要求需要遵循协议的类型必须是引用语义而非值语义时,应该采用类类型专属协议。关于引用语义和值语义的更多内容,请查看 结构体和枚举是值类型类是引用类型

协议合成

要求一个类型同时遵循多个协议是很有用的。你可以使用协议组合来复合多个协议到一个要求里。协议组合行为就和你定义的临时局部协议一样拥有构成中所有协议的需求。协议组合不定义任何新的协议类型。

协议组合使用 SomeProtocol & AnotherProtocol 的形式。你可以列举任意数量的协议,用和符号(&)分开。除了协议列表,协议组合也能包含类类型,这允许你标明一个需要的父类。

下面的例子中,将 NamedAged 两个协议按照上述语法组合成一个协议,作为函数参数的类型:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
protocol Named {
var name: String { get }
}
protocol Aged {
var age: Int { get }
}
struct Person: Named, Aged {
var name: String
var age: Int
}
func wishHappyBirthday(to celebrator: Named & Aged) {
print("Happy birthday, \(celebrator.name), you're \(celebrator.age)!")
}
let birthdayPerson = Person(name: "Malcolm", age: 21)
wishHappyBirthday(to: birthdayPerson)
// 打印 “Happy birthday Malcolm - you're 21!”

Named 协议包含 String 类型的 name 属性。Aged 协议包含 Int 类型的 age 属性。Person 结构体采纳了这两个协议。

wishHappyBirthday(to:) 函数的参数 celebrator 的类型为 Named & Aged, 这意味着“任何同时遵循 Named 和 Aged 的协议”。它不关心参数的具体类型,只要参数符合这两个协议即可。

上面的例子创建了一个名为 birthdayPersonPerson 的实例,作为参数传递给了 wishHappyBirthday(to:) 函数。因为 Person 同时符合这两个协议,所以这个参数合法,函数将打印生日问候语。

这里有一个例子:将 Location 类和前面的 Named 协议进行组合:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
class Location {
var latitude: Double
var longitude: Double
init(latitude: Double, longitude: Double) {
self.latitude = latitude
self.longitude = longitude
}
}
class City: Location, Named {
var name: String
init(name: String, latitude: Double, longitude: Double) {
self.name = name
super.init(latitude: latitude, longitude: longitude)
}
}
func beginConcert(in location: Location & Named) {
print("Hello, \(location.name)!")
}

let seattle = City(name: "Seattle", latitude: 47.6, longitude: -122.3)
beginConcert(in: seattle)
// 打印 "Hello, Seattle!"

beginConcert(in:) 函数接受一个类型为 Location & Named 的参数,这意味着“任何 Location 的子类,并且遵循 Named 协议”。例如,City 就满足这样的条件。

将 birthdayPerson 传入 beginConcert(in:) 函数是不合法的,因为 Person 不是 Location 的子类。同理,如果你新建一个类继承于 Location,但是没有遵循 Named 协议,而用这个类的实例去调用 beginConcert(in:) 函数也是非法的。

检查协议一致性

你可以使用 类型转换 中描述的 isas 操作符来检查协议一致性,即是否符合某协议,并且可以转换到指定的协议类型。检查和转换协议的语法与检查和转换类型是完全一样的:

  • is 用来检查实例是否符合某个协议,若符合则返回 true,否则返回 false
  • as? 返回一个可选值,当实例符合某个协议时,返回类型为协议类型的可选值,否则返回 nil
  • as! 将实例强制向下转换到某个协议类型,如果强转失败,将触发运行时错误。

下面的例子定义了一个 HasArea 协议,该协议定义了一个 Double 类型的可读属性 area

1
2
3
protocol HasArea {
var area: Double { get }
}

如下所示,Circle 类和 Country 类都遵循了 HasArea 协议:

1
2
3
4
5
6
7
8
9
10
class Circle: HasArea {
let pi = 3.1415927
var radius: Double
var area: Double { return pi * radius * radius }
init(radius: Double) { self.radius = radius }
}
class Country: HasArea {
var area: Double
init(area: Double) { self.area = area }
}

Circle 类把 area 属性实现为基于存储型属性 radius 的计算型属性。Country 类则把 area 属性实现为存储型属性。这两个类都正确地遵循了 HasArea 协议。

如下所示,Animal 是一个未遵循 HasArea 协议的类:

1
2
3
4
class Animal {
var legs: Int
init(legs: Int) { self.legs = legs }
}

CircleCountryAnimal 并没有一个共同的基类,尽管如此,它们都是类,它们的实例都可以作为 AnyObject 类型的值,存储在同一个数组中:

1
2
3
4
5
let objects: [AnyObject] = [
Circle(radius: 2.0),
Country(area: 243_610),
Animal(legs: 4)
]

objects 数组使用字面量初始化,数组包含一个 radius2Circle 的实例,一个保存了英国国土面积的 Country 实例和一个 legs4Animal 实例。

如下所示,objects 数组可以被迭代,并对迭代出的每一个元素进行检查,看它是否符合 HasArea 协议:

1
2
3
4
5
6
7
8
9
10
for object in objects {
if let objectWithArea = object as? HasArea {
print("Area is \(objectWithArea.area)")
} else {
print("Something that doesn't have an area")
}
}
// Area is 12.5663708
// Area is 243610.0
// Something that doesn't have an area

当迭代出的元素符合 HasArea 协议时,将 as? 操作符返回的可选值通过可选绑定,绑定到 objectWithArea 常量上。objectWithAreaHasArea 协议类型的实例,因此 area 属性可以被访问和打印。

objects 数组中的元素的类型并不会因为强转而丢失类型信息,它们仍然是 CircleCountryAnimal 类型。然而,当它们被赋值给 objectWithArea 常量时,只被视为 HasArea 类型,因此只有 area 属性能够被访问。

可选的协议要求

协议可以定义可选要求,遵循协议的类型可以选择是否实现这些要求。在协议中使用 optional 关键字作为前缀来定义可选要求。可选要求用在你需要和 Objective-C 打交道的代码中。协议和可选要求都必须带上 @objc 属性。标记 @objc 特性的协议只能被继承自 Objective-C 类的类或者 @objc 类遵循,其他类以及结构体和枚举均不能遵循这种协议。

使用可选要求时(例如,可选的方法或者属性),它们的类型会自动变成可选的。比如,一个类型为 (Int) -> String 的方法会变成 ((Int) -> String)?。需要注意的是整个函数类型是可选的,而不是函数的返回值。

协议中的可选要求可通过可选链式调用来使用,因为遵循协议的类型可能没有实现这些可选要求。类似 someOptionalMethod?(someArgument) 这样,你可以在可选方法名称后加上 ? 来调用可选方法。详细内容可在 可选链式调用 章节中查看。

下面的例子定义了一个名为 Counter 的用于整数计数的类,它使用外部的数据源来提供每次的增量。数据源由 CounterDataSource 协议定义,它包含两个可选要求:

1
2
3
4
@objc protocol CounterDataSource {
@objc optional func increment(forCount count: Int) -> Int
@objc optional var fixedIncrement: Int { get }
}

CounterDataSource 协议定义了一个可选方法 increment(forCount:) 和一个可选属性 fiexdIncrement,它们使用了不同的方法来从数据源中获取适当的增量值。

注意

严格来讲,CounterDataSource 协议中的方法和属性都是可选的,因此遵循协议的类可以不实现这些要求,尽管技术上允许这样做,不过最好不要这样写。

Counter 类含有 CounterDataSource? 类型的可选属性 dataSource,如下所示:

1
2
3
4
5
6
7
8
9
10
11
class Counter {
var count = 0
var dataSource: CounterDataSource?
func increment() {
if let amount = dataSource?.increment?(forCount: count) {
count += amount
} else if let amount = dataSource?.fixedIncrement {
count += amount
}
}
}

Counter 类使用变量属性 count 来存储当前值。该类还定义了一个 increment 方法,每次调用该方法的时候,将会增加 count 的值。

increment() 方法首先试图使用 increment(forCount:) 方法来得到每次的增量。increment() 方法使用可选链式调用来尝试调用 increment(forCount:),并将当前的 count 值作为参数传入。

这里使用了两层可选链式调用。首先,由于 dataSource 可能为 nil,因此在 dataSource 后边加上了 ?,以此表明只在 dataSource 非空时才去调用 increment(forCount:) 方法。其次,即使 dataSource 存在,也无法保证其是否实现了 increment(forCount:) 方法,因为这个方法是可选的。因此,increment(forCount:) 方法同样使用可选链式调用进行调用,只有在该方法被实现的情况下才能调用它,所以在 increment(forCount:) 方法后边也加上了 ?

调用 increment(forCount:) 方法在上述两种情形下都有可能失败,所以返回值为 Int? 类型。虽然在 CounterDataSource 协议中,increment(forCount:) 的返回值类型是非可选 Int。另外,即使这里使用了两层可选链式调用,最后的返回结果依旧是单层的可选类型。关于这一点的更多信息,请查阅 连接多层可选链式调用

在调用 increment(forCount:) 方法后,Int? 型的返回值通过可选绑定解包并赋值给常量 amount。如果可选值确实包含一个数值,也就是说,数据源和方法都存在,数据源方法返回了一个有效值。之后便将解包后的 amount 加到 count 上,增量操作完成。

如果没有从 increment(forCount:) 方法获取到值,可能由于 dataSourcenil,或者它并没有实现 increment(forCount:) 方法,那么 increment() 方法将试图从数据源的 fixedIncrement 属性中获取增量。fixedIncrement 是一个可选属性,因此属性值是一个 Int? 值,即使该属性在 CounterDataSource 协议中的类型是非可选的 Int

下面的例子展示了 CounterDataSource 的简单实现。ThreeSource 类遵循了 CounterDataSource 协议,它实现了可选属性 fixedIncrement,每次会返回 3

1
2
3
class ThreeSource: NSObject, CounterDataSource {
let fixedIncrement = 3
}

可以使用 ThreeSource 的实例作为 Counter 实例的数据源:

1
2
3
4
5
6
7
8
9
10
var counter = Counter()
counter.dataSource = ThreeSource()
for _ in 1...4 {
counter.increment()
print(counter.count)
}
// 3
// 6
// 9
// 12

上述代码新建了一个 Counter 实例,并将它的数据源设置为一个 ThreeSource 的实例,然后调用 increment() 方法 4 次。按照预期预期一样,每次调用都会将 count 的值增加 3.

下面是一个更为复杂的数据源 TowardsZeroSource,它将使得最后的值变为 0

1
2
3
4
5
6
7
8
9
10
11
class TowardsZeroSource: NSObject, CounterDataSource {
func increment(forCount count: Int) -> Int {
if count == 0 {
return 0
} else if count < 0 {
return 1
} else {
return -1
}
}
}

TowardsZeroSource 实现了 CounterDataSource 协议中的 increment(forCount:) 方法,以 count 参数为依据,计算出每次的增量。如果 count 已经为 0,此方法将返回 0,以此表明之后不应再有增量操作发生。

你可以使用 TowardsZeroSource 实例将 Counter 实例来从 -4 增加到 0。一旦增加到 0,数值便不会再有变动:

1
2
3
4
5
6
7
8
9
10
11
counter.count = -4
counter.dataSource = TowardsZeroSource()
for _ in 1...5 {
counter.increment()
print(counter.count)
}
// -3
// -2
// -1
// 0
// 0

协议扩展

协议可以通过扩展来为遵循协议的类型提供属性、方法以及下标的实现。通过这种方式,你可以基于协议本身来实现这些功能,而无需在每个遵循协议的类型中都重复同样的实现,也无需使用全局函数。

例如,可以扩展 RandomNumberGenerator 协议来提供 randomBool() 方法。该方法使用协议中定义的 random() 方法来返回一个随机的 Bool 值:

1
2
3
4
5
extension RandomNumberGenerator {
func randomBool() -> Bool {
return random() > 0.5
}
}

通过协议扩展,所有遵循协议的类型,都能自动获得这个扩展所增加的方法实现而无需任何额外修改:

1
2
3
4
5
let generator = LinearCongruentialGenerator()
print("Here's a random number: \(generator.random())")
// 打印 “Here's a random number: 0.37464991998171”
print("And here's a random Boolean: \(generator.randomBool())")
// 打印 “And here's a random Boolean: true”

提供默认实现

可以通过协议扩展来为协议要求的属性、方法以及下标提供默认的实现。如果遵循协议的类型为这些要求提供了自己的实现,那么这些自定义实现将会替代扩展中的默认实现被使用。

注意

通过协议扩展为协议要求提供的默认实现和可选的协议要求不同。虽然在这两种情况下,遵循协议的类型都无需自己实现这些要求,但是通过扩展提供的默认实现可以直接调用,而无需使用可选链式调用。

例如,PrettyTextRepresentable 协议继承自 TextRepresentable 协议,可以为其提供一个默认的 prettyTextualDescription 属性来简单地返回 textualDescription 属性的值:

1
2
3
4
5
extension PrettyTextRepresentable  {
var prettyTextualDescription: String {
return textualDescription
}
}

为协议扩展添加限制条件

在扩展协议的时候,可以指定一些限制条件,只有遵循协议的类型满足这些限制条件时,才能获得协议扩展提供的默认实现。这些限制条件写在协议名之后,使用 where 子句来描述,正如 泛型 Where 子句 中所描述的。

例如,你可以扩展 Collection 协议,适用于集合中的元素遵循了 Equatable 协议的情况。通过限制集合元素遵 Equatable 协议, 作为标准库的一部分, 你可以使用 ==!= 操作符来检查两个元素的等价性和非等价性。

1
2
3
4
5
6
7
8
9
10
extension Collection where Element: Equatable {
func allEqual() -> Bool {
for element in self {
if element != self.first {
return false
}
}
return true
}
}

如果集合中的所有元素都一致,allEqual() 方法才返回 true

看看两个整数数组,一个数组的所有元素都是一样的,另一个不一样:

1
2
let equalNumbers = [100, 100, 100, 100, 100]
let differentNumbers = [100, 100, 200, 100, 200]

由于数组遵循 Collection 而且整数遵循 EquatableequalNumbersdifferentNumbers 都可以使用 allEqual() 方法。

1
2
3
4
print(equalNumbers.allEqual())
// 打印 "true"
print(differentNumbers.allEqual())
// 打印 "false"

注意

如果一个遵循的类型满足了为同一方法或属性提供实现的多个限制型扩展的要求, Swift 会使用最匹配限制的实现。

留言與分享

swift扩展

分類 编程语言, swift

扩展

扩展可以给一个现有的类,结构体,枚举,还有协议添加新的功能。它还拥有不需要访问被扩展类型源代码就能完成扩展的能力(即逆向建模)。扩展和 Objective-C 的分类很相似。(与 Objective-C 分类不同的是,Swift 扩展是没有名字的。)

Swift 中的扩展可以:

  • 添加计算型实例属性和计算型类属性
  • 定义实例方法和类方法
  • 提供新的构造器
  • 定义下标
  • 定义和使用新的嵌套类型
  • 使已经存在的类型遵循(conform)一个协议

在 Swift 中,你甚至可以扩展协议以提供其需要的实现,或者添加额外功能给遵循的类型所使用。你可以从 协议扩展 获取更多细节。

注意

扩展可以给一个类型添加新的功能,但是不能重写已经存在的功能。

扩展的语法

使用 extension 关键字声明扩展:

1
2
3
extension SomeType {
// 在这里给 SomeType 添加新的功能
}

扩展可以扩充一个现有的类型,给它添加一个或多个协议。协议名称的写法和类或者结构体一样:

1
2
3
extension SomeType: SomeProtocol, AnotherProtocol {
// 协议所需要的实现写在这里
}

这种遵循协议的方式在 使用扩展遵循协议 中有描述。

扩展可以使用在现有范型类型上,就像 扩展范型类型 中描述的一样。你还可以使用扩展给泛型类型有条件的添加功能,就像 扩展一个带有 Where 字句的范型 中描述的一样。

注意

对一个现有的类型,如果你定义了一个扩展来添加新的功能,那么这个类型的所有实例都可以使用这个新功能,包括那些在扩展定义之前就存在的实例。

计算型属性

扩展可以给现有类型添加计算型实例属性和计算型类属性。这个例子给 Swift 内建的 Double 类型添加了五个计算型实例属性,从而提供与距离单位相关工作的基本支持:

1
2
3
4
5
6
7
8
9
10
11
12
13
extension Double {
var km: Double { return self * 1_000.0 }
var m: Double { return self }
var cm: Double { return self / 100.0 }
var mm: Double { return self / 1_000.0 }
var ft: Double { return self / 3.28084 }
}
let oneInch = 25.4.mm
print("One inch is \(oneInch) meters")
// 打印“One inch is 0.0254 meters”
let threeFeet = 3.ft
print("Three feet is \(threeFeet) meters")
// 打印“Three feet is 0.914399970739201 meters”

这些计算型属性表示的含义是把一个 Double 值看作是某单位下的长度值。即使它们被实现为计算型属性,但这些属性的名字仍可紧接一个浮点型字面值,从而通过点语法来使用,并以此实现距离转换。

在上述例子中,Double 类型的 1.0 代表的是“一米”。这就是为什么计算型属性 m 返回的是 self——表达式 1.m 被认为是计算一个 Double 类型的 1.0

其它单位则需要一些单位换算。一千米等于 1,000 米,所以计算型属性 km 要把值乘以 1_000.00 来实现千米到米的单位换算。类似地,一米有 3.28084 英尺,所以计算型属性 ft 要把对应的 Double 值除以 3.28084,来实现英尺到米的单位换算。

这些属性都是只读的计算型属性,所以为了简便,它们的表达式里面都不包含 get 关键字。它们使用 Double 作为返回值类型,并可用于所有接受 Double 类型的数学计算中:

1
2
3
let aMarathon = 42.km + 195.m
print("A marathon is \(aMarathon) meters long")
// 打印“A marathon is 42195.0 meters long”

注意

扩展可以添加新的计算属性,但是它们不能添加存储属性,或向现有的属性添加属性观察者。

构造器

扩展可以给现有的类型添加新的构造器。它使你可以把自定义类型作为参数来供其他类型的构造器使用,或者在类型的原始实现上添加额外的构造选项。

扩展可以给一个类添加新的便利构造器,但是它们不能给类添加新的指定构造器或者析构器。指定构造器和析构器必须始终由类的原始实现提供。

如果你使用扩展给一个值类型添加构造器只是用于给所有的存储属性提供默认值,并且没有定义任何自定义构造器,那么你可以在该值类型扩展的构造器中使用默认构造器和成员构造器。如果你把构造器写到了值类型的原始实现中,就像 值类型的构造器委托 中所描述的,那么就不属于在扩展中添加构造器。

如果你使用扩展给另一个模块中定义的结构体添加构造器,那么新的构造器直到定义模块中使用一个构造器之前,不能访问 self

在下面的例子中,自定义了一个的 Rect 结构体用来表示一个几何矩形。这个例子中还定义了两个给予支持的结构体 SizePoint,它们都把属性的默认值设置为 0.0

1
2
3
4
5
6
7
8
9
10
struct Size {
var width = 0.0, height = 0.0
}
struct Point {
var x = 0.0, y = 0.0
}
struct Rect {
var origin = Point()
var size = Size()
}

因为 Rect 结构体给所有的属性都提供了默认值,所以它自动获得了一个默认构造器和一个成员构造器,就像 默认构造器 中描述的一样。这些构造器可以用来创建新的 Rect 实例:

1
2
3
let defaultRect = Rect()
let memberwiseRect = Rect(origin: Point(x: 2.0, y: 2.0),
size: Size(width: 5.0, height: 5.0))

你可以通过扩展 Rect 结构体来提供一个允许指定 point 和 size 的构造器:

1
2
3
4
5
6
7
extension Rect {
init(center: Point, size: Size) {
let originX = center.x - (size.width / 2)
let originY = center.y - (size.height / 2)
self.init(origin: Point(x: originX, y: originY), size: size)
}
}

这个新的构造器首先根据提供的 centersize 计算一个适当的原点。然后这个构造器调用结构体自带的成员构造器 init(origin:size:),它会将新的 origin 和 size 值储存在适当的属性中:

1
2
3
let centerRect = Rect(center: Point(x: 4.0, y: 4.0),
size: Size(width: 3.0, height: 3.0))
// centerRect 的 origin 是 (2.5, 2.5) 并且它的 size 是 (3.0, 3.0)

注意

如果你通过扩展提供一个新的构造器,你有责任确保每个通过该构造器创建的实例都是初始化完整的。

方法

扩展可以给现有类型添加新的实例方法和类方法。在下面的例子中,给 Int 类型添加了一个新的实例方法叫做 repetitions

1
2
3
4
5
6
7
extension Int {
func repetitions(task: () -> Void) {
for _ in 0..<self {
task()
}
}
}

repetitions(task:) 方法仅接收一个 () -> Void 类型的参数,它表示一个没有参数没有返回值的方法。

定义了这个扩展之后,你可以对任意整形数值调用 repetitions(task:) 方法,来执行对应次数的任务:

1
2
3
4
5
6
3.repetitions {
print("Hello!")
}
// Hello!
// Hello!
// Hello!

可变实例方法

通过扩展添加的实例方法同样也可以修改(或 mutating(改变))实例本身。结构体和枚举的方法,若是可以修改 self 或者它自己的属性,则必须将这个实例方法标记为 mutating,就像是改变了方法的原始实现。

在下面的例子中,对 Swift 的 Int 类型添加了一个新的 mutating 方法,叫做 square,它将原始值求平方:

1
2
3
4
5
6
7
8
extension Int {
mutating func square() {
self = self * self
}
}
var someInt = 3
someInt.square()
// someInt 现在是 9

下标

扩展可以给现有的类型添加新的下标。下面的例子中,对 Swift 的 Int 类型添加了一个整数类型的下标。下标 [n] 从数字右侧开始,返回小数点后的第 n 位:

  • 123456789[0] 返回 9
  • 123456789[1] 返回 8

……以此类推:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
extension Int {
subscript(digitIndex: Int) -> Int {
var decimalBase = 1
for _ in 0..<digitIndex {
decimalBase *= 10
}
return (self / decimalBase) % 10
}
}
746381295[0]
// 返回 5
746381295[1]
// 返回 9
746381295[2]
// 返回 2
746381295[8]
// 返回 7

如果操作的 Int 值没有足够的位数满足所请求的下标,那么下标的现实将返回 0,将好像在数字的左边补上了 0:

1
2
3
746381295[9]
// 返回 0,就好像你进行了这个请求:
0746381295[9]

嵌套类型

扩展可以给现有的类,结构体,还有枚举添加新的嵌套类型:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
extension Int {
enum Kind {
case negative, zero, positive
}
var kind: Kind {
switch self {
case 0:
return .zero
case let x where x > 0:
return .positive
default:
return .negative
}
}
}

这个例子给 Int 添加了一个新的嵌套枚举。这个枚举叫做 Kind,表示特定整数所代表的数字类型。具体来说,它表示数字是负的、零的还是正的。

这个例子同样给 Int 添加了一个新的计算型实例属性,叫做 kind,它返回被操作整数所对应的 Kind 枚举 case 分支。

现在,任意 Int 的值都可以使用这个嵌套类型:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
func printIntegerKinds(_ numbers: [Int]) {
for number in numbers {
switch number.kind {
case .negative:
print("- ", terminator: "")
case .zero:
print("0 ", terminator: "")
case .positive:
print("+ ", terminator: "")
}
}
print("")
}
printIntegerKinds([3, 19, -27, 0, -6, 0, 7])
// 打印“+ + - 0 - 0 + ”

方法 printIntegerKinds(_:),使用一个 Int 类型的数组作为输入,然后依次迭代这些值。对于数组中的每一个整数,方法会检查它的 kind 计算型属性,然后打印适当的描述。

注意

number.kind 已经被认为是 Int.Kind 类型。所以,在 switch 语句中所有的 Int.Kind case 分支可以被缩写,就像使用 .negative 替代 Int.Kind.negative.

留言與分享

swift嵌套类型

分類 编程语言, swift

嵌套类型

枚举常被用于为特定类或结构体实现某些功能。类似地,枚举可以方便的定义工具类或结构体,从而为某个复杂的类型所使用。为了实现这种功能,Swift 允许你定义嵌套类型,可以在支持的类型中定义嵌套的枚举、类和结构体。

要在一个类型中嵌套另一个类型,将嵌套类型的定义写在其外部类型的 {} 内,而且可以根据需要定义多级嵌套。

嵌套类型实践

下面这个例子定义了一个结构体 BlackjackCard(二十一点),用来模拟 BlackjackCard 中的扑克牌点数。BlackjackCard 结构体包含两个嵌套定义的枚举类型 SuitRank

BlackjackCard 中,Ace 牌可以表示 1 或者 11Ace 牌的这一特征通过一个嵌套在 Rank 枚举中的结构体 Values 来表示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
struct BlackjackCard {

// 嵌套的 Suit 枚举
enum Suit: Character {
case spades = "♠", hearts = "♡", diamonds = "♢", clubs = "♣"
}

// 嵌套的 Rank 枚举
enum Rank: Int {
case two = 2, three, four, five, six, seven, eight, nine, ten
case jack, queen, king, ace
struct Values {
let first: Int, second: Int?
}
var values: Values {
switch self {
case .ace:
return Values(first: 1, second: 11)
case .jack, .queen, .king:
return Values(first: 10, second: nil)
default:
return Values(first: self.rawValue, second: nil)
}
}
}

// BlackjackCard 的属性和方法
let rank: Rank, suit: Suit
var description: String {
var output = "suit is \(suit.rawValue),"
output += " value is \(rank.values.first)"
if let second = rank.values.second {
output += " or \(second)"
}
return output
}
}

Suit 枚举用来描述扑克牌的四种花色,并用一个 Character 类型的原始值表示花色符号。

Rank 枚举用来描述扑克牌从 Ace~10,以及 JQK,这 13 种牌,并用一个 Int 类型的原始值表示牌的面值。(这个 Int 类型的原始值未用于 AceJQK4 种牌。)

如上所述,Rank 枚举在内部定义了一个嵌套结构体 Values。结构体 Values 中定义了两个属性,用于反映只有 Ace 有两个数值,其余牌都只有一个数值:

  • first 的类型为 Int
  • second 的类型为 Int?,或者说“可选 Int

Rank 还定义了一个计算型属性 values,它将会返回一个 Values 结构体的实例。这个计算型属性会根据牌的面值,用适当的数值去初始化 Values 实例。对于 JQKAce 这四种牌,会使用特殊数值。对于数字面值的牌,使用枚举实例的 Int 类型的原始值。

BlackjackCard 结构体拥有两个属性——ranksuit。它也同样定义了一个计算型属性 descriptiondescription 属性用 ranksuit 中的内容来构建对扑克牌名字和数值的描述。该属性使用可选绑定来检查可选类型 second 是否有值,若有值,则在原有的描述中增加对 second 的描述。

因为 BlackjackCard 是一个没有自定义构造器的结构体,在 结构体的逐一成员构造器 中可知,结构体有默认的成员构造器,所以你可以用默认的构造器去初始化新常量 theAceOfSpades

1
2
3
let theAceOfSpades = BlackjackCard(rank: .ace, suit: .spades)
print("theAceOfSpades: \(theAceOfSpades.description)")
// 打印“theAceOfSpades: suit is ♠, value is 1 or 11”

尽管 RankSuit 嵌套在 BlackjackCard 中,但它们的类型仍可从上下文中推断出来,所以在初始化实例时能够单独通过成员名称(.ace.spades)引用枚举实例。在上面的例子中,description 属性正确地反映了黑桃 A 牌具有 111 两个值。

引用嵌套类型

在外部引用嵌套类型时,在嵌套类型的类型名前加上其外部类型的类型名作为前缀:

1
2
let heartsSymbol = BlackjackCard.Suit.hearts.rawValue
// 红心符号为“♡”

对于上面这个例子,这样可以使 SuitRankValues 的名字尽可能的短,因为它们的名字可以由定义它们的上下文来限定。

留言與分享

swift类型转换

分類 编程语言, swift

类型转换

类型转换可以判断实例的类型,也可以将实例看做是其父类或者子类的实例。

类型转换在 Swift 中使用 isas 操作符实现。这两个操作符分别提供了一种简单达意的方式去检查值的类型或者转换它的类型。

你也可以用它来检查一个类型是否遵循了某个协议,就像在 检验协议遵循 部分讲述的一样。

为类型转换定义类层次

你可以将类型转换用在类和子类的层次结构上,检查特定类实例的类型并且转换这个类实例的类型成为这个层次结构中的其他类型。下面的三个代码段定义了一个类层次和一个包含了这些类实例的数组,作为类型转换的例子。

第一个代码片段定义了一个新的基类 MediaItem。这个类为任何出现在数字媒体库的媒体项提供基础功能。特别的,它声明了一个 String 类型的 name 属性,和一个 init(name:) 初始化器。(假定所有的媒体项都有个名称。)

1
2
3
4
5
6
class MediaItem {
var name: String
init(name: String) {
self.name = name
}
}

下一个代码段定义了 MediaItem 的两个子类。第一个子类 Movie 封装了与电影相关的额外信息,在父类(或者说基类)的基础上增加了一个 director(导演)属性,和相应的初始化器。第二个子类 Song,在父类的基础上增加了一个 artist(艺术家)属性,和相应的初始化器:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class Movie: MediaItem {
var director: String
init(name: String, director: String) {
self.director = director
super.init(name: name)
}
}

class Song: MediaItem {
var artist: String
init(name: String, artist: String) {
self.artist = artist
super.init(name: name)
}
}

最后一个代码段创建了一个数组常量 library,包含两个 Movie 实例和三个 Song 实例。library 的类型是在它被初始化时根据它数组中所包含的内容推断来的。Swift 的类型检测器能够推断出 MovieSong 有共同的父类 MediaItem,所以它推断出 [MediaItem] 类作为 library 的类型:

1
2
3
4
5
6
7
8
let library = [
Movie(name: "Casablanca", director: "Michael Curtiz"),
Song(name: "Blue Suede Shoes", artist: "Elvis Presley"),
Movie(name: "Citizen Kane", director: "Orson Welles"),
Song(name: "The One And Only", artist: "Chesney Hawkes"),
Song(name: "Never Gonna Give You Up", artist: "Rick Astley")
]
// 数组 library 的类型被推断为 [MediaItem]

在幕后 library 里存储的媒体项依然是 MovieSong 类型的。但是,若你迭代它,依次取出的实例会是 MediaItem 类型的,而不是 MovieSong 类型。为了让它们作为原本的类型工作,你需要检查它们的类型或者向下转换它们到其它类型,就像下面描述的一样。

检查类型

类型检查操作符is)来检查一个实例是否属于特定子类型。若实例属于那个子类型,类型检查操作符返回 true,否则返回 false

下面的例子定义了两个变量,movieCountsongCount,用来计算数组 libraryMovieSong 类型的实例数量:

1
2
3
4
5
6
7
8
9
10
11
12
13
var movieCount = 0
var songCount = 0

for item in library {
if item is Movie {
movieCount += 1
} else if item is Song {
songCount += 1
}
}

print("Media library contains \(movieCount) movies and \(songCount) songs")
// 打印“Media library contains 2 movies and 3 songs”

示例迭代了数组 library 中的所有项。每一次,for-in 循环设置 item 常量为数组中的下一个 MediaItem 实例。

若当前 MediaItem 是一个 Movie 类型的实例,item is Movie 返回 true,否则返回 false。同样的,item is Song 检查 item 是否为 Song 类型的实例。在循环结束后,movieCountsongCount 的值就是被找到的属于各自类型的实例的数量。

向下转型

某类型的一个常量或变量可能在幕后实际上属于一个子类。当确定是这种情况时,你可以尝试用类型转换操作符as?as!)向下转到它的子类型。

因为向下转型可能会失败,类型转型操作符带有两种不同形式。条件形式 as? 返回一个你试图向下转成的类型的可选值。强制形式 as! 把试图向下转型和强制解包转换结果结合为一个操作。

当你不确定向下转型可以成功时,用类型转换的条件形式(as?)。条件形式的类型转换总是返回一个可选值,并且若下转是不可能的,可选值将是 nil。这使你能够检查向下转型是否成功。

只有你可以确定向下转型一定会成功时,才使用强制形式(as!)。当你试图向下转型为一个不正确的类型时,强制形式的类型转换会触发一个运行时错误。

下面的例子,迭代了 library 里的每一个 MediaItem,并打印出适当的描述。要这样做,item 需要真正作为 MovieSong 的类型来使用,而不仅仅是作为 MediaItem。为了能够在描述中使用 MovieSongdirectorartist 属性,这是必要的。

在这个示例中,数组中的每一个 item 可能是 MovieSong。事前你不知道每个 item 的真实类型,所以这里使用条件形式的类型转换(as?)去检查循环里的每次下转:

1
2
3
4
5
6
7
8
9
10
11
12
13
for item in library {
if let movie = item as? Movie {
print("Movie: \(movie.name), dir. \(movie.director)")
} else if let song = item as? Song {
print("Song: \(song.name), by \(song.artist)")
}
}

// Movie: Casablanca, dir. Michael Curtiz
// Song: Blue Suede Shoes, by Elvis Presley
// Movie: Citizen Kane, dir. Orson Welles
// Song: The One And Only, by Chesney Hawkes
// Song: Never Gonna Give You Up, by Rick Astley

示例首先试图将 item 下转为 Movie。因为 item 是一个 MediaItem 类型的实例,它可能是一个 Movie;同样,它也可能是一个 Song,或者仅仅是基类 MediaItem。因为不确定,as? 形式在试图下转时将返回一个可选值。item as? Movie 的返回值是 Movie? 或者说“可选 Movie”。

当向下转型为 Movie 应用在两个 Song 实例时将会失败。为了处理这种情况,上面的例子使用了可选绑定(optional binding)来检查可选 Movie 真的包含一个值(这个是为了判断下转是否成功。)可选绑定是这样写的“if let movie = item as? Movie”,可以这样解读:

“尝试将 item 转为 Movie 类型。若成功,设置一个新的临时常量 movie 来存储返回的可选 Movie 中的值”

若向下转型成功,然后 movie 的属性将用于打印一个 Movie 实例的描述,包括它的导演的名字 director。相似的原理被用来检测 Song 实例,当 Song 被找到时则打印它的描述(包含 artist 的名字)。

注意

转换没有真的改变实例或它的值。根本的实例保持不变;只是简单地把它作为它被转换成的类型来使用。

AnyAnyObject 的类型转换

Swift 为不确定类型提供了两种特殊的类型别名:

  • Any 可以表示任何类型,包括函数类型。
  • AnyObject 可以表示任何类类型的实例。

只有当你确实需要它们的行为和功能时才使用 AnyAnyObject。最好还是在代码中指明需要使用的类型。

这里有个示例,使用 Any 类型来和混合的不同类型一起工作,包括函数类型和非类类型。它创建了一个可以存储 Any 类型的数组 things

1
2
3
4
5
6
7
8
9
10
var things = [Any]()

things.append(0)
things.append(0.0)
things.append(42)
things.append(3.14159)
things.append("hello")
things.append((3.0, 5.0))
things.append(Movie(name: "Ghostbusters", director: "Ivan Reitman"))
things.append({ (name: String) -> String in "Hello, \(name)" })

things 数组包含两个 Int 值,两个 Double 值,一个 String 值,一个元组 (Double, Double),一个 Movie 实例“Ghostbusters”,以及一个接受 String 值并返回另一个 String 值的闭包表达式。

你可以在 switch 表达式的 case 中使用 isas 操作符来找出只知道是 AnyAnyObject 类型的常量或变量的具体类型。下面的示例迭代 things 数组中的每一项,并用 switch 语句查找每一项的类型。有几个 switch 语句的 case 绑定它们匹配到的值到一个指定类型的常量,从而可以打印这些值:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
for thing in things {
switch thing {
case 0 as Int:
print("zero as an Int")
case 0 as Double:
print("zero as a Double")
case let someInt as Int:
print("an integer value of \(someInt)")
case let someDouble as Double where someDouble > 0:
print("a positive double value of \(someDouble)")
case is Double:
print("some other double value that I don't want to print")
case let someString as String:
print("a string value of \"\(someString)\"")
case let (x, y) as (Double, Double):
print("an (x, y) point at \(x), \(y)")
case let movie as Movie:
print("a movie called \(movie.name), dir. \(movie.director)")
case let stringConverter as (String) -> String:
print(stringConverter("Michael"))
default:
print("something else")
}
}

// zero as an Int
// zero as a Double
// an integer value of 42
// a positive double value of 3.14159
// a string value of "hello"
// an (x, y) point at 3.0, 5.0
// a movie called Ghostbusters, dir. Ivan Reitman
// Hello, Michael

注意

Any 类型可以表示所有类型的值,包括可选类型。Swift 会在你用 Any 类型来表示一个可选值的时候,给你一个警告。如果你确实想使用 Any 类型来承载可选值,你可以使用 as 操作符显式转换为 Any,如下所示:

1
2
3
let optionalNumber: Int? = 3
things.append(optionalNumber) // 警告
things.append(optionalNumber as Any) // 没有警告

留言與分享

swift错误处理

分類 编程语言, swift

错误处理

错误处理(Error handling) 是响应错误以及从错误中恢复的过程。Swift 在运行时提供了抛出、捕获、传递和操作可恢复错误(recoverable errors)的一等支持(first-class support)。

某些操作无法保证总是执行完所有代码或生成有用的结果。可选类型用来表示值缺失,但是当某个操作失败时,理解造成失败的原因有助于你的代码作出相应的应对。

举个例子,假如有个从磁盘上的某个文件读取数据并进行处理的任务,该任务会有多种可能失败的情况,包括指定路径下文件并不存在,文件不具有可读权限,或者文件编码格式不兼容。区分这些不同的失败情况可以让程序处理并解决某些错误,然后把它解决不了的错误报告给用户。

注意

Swift 中的错误处理涉及到错误处理模式,这会用到 Cocoa 和 Objective-C 中的 NSError。更多详情参见 用 Swift 解决 Cocoa 错误

表示与抛出错误

在 Swift 中,错误用遵循 Error 协议的类型的值来表示。这个空协议表明该类型可以用于错误处理。

Swift 的枚举类型尤为适合构建一组相关的错误状态,枚举的关联值还可以提供错误状态的额外信息。例如,在游戏中操作自动贩卖机时,你可以这样表示可能会出现的错误状态:

1
2
3
4
5
enum VendingMachineError: Error {
case invalidSelection //选择无效
case insufficientFunds(coinsNeeded: Int) //金额不足
case outOfStock //缺货
}

抛出一个错误可以让你表明有意外情况发生,导致正常的执行流程无法继续执行。抛出错误使用 throw 语句。例如,下面的代码抛出一个错误,提示贩卖机还需要 5 个硬币:

1
throw VendingMachineError.insufficientFunds(coinsNeeded: 5)

处理错误

某个错误被抛出时,附近的某部分代码必须负责处理这个错误,例如纠正这个问题、尝试另外一种方式、或是向用户报告错误。

Swift 中有 4 种处理错误的方式。你可以把函数抛出的错误传递给调用此函数的代码、用 do-catch 语句处理错误、将错误作为可选类型处理、或者断言此错误根本不会发生。每种方式在下面的小节中都有描述。

当一个函数抛出一个错误时,你的程序流程会发生改变,所以重要的是你能迅速识别代码中会抛出错误的地方。为了标识出这些地方,在调用一个能抛出错误的函数、方法或者构造器之前,加上 try 关键字,或者 try?try! 这种变体。这些关键字在下面的小节中有具体讲解。

注意

Swift 中的错误处理和其他语言中用 trycatchthrow 进行异常处理很像。和其他语言中(包括 Objective-C )的异常处理不同的是,Swift 中的错误处理并不涉及解除调用栈,这是一个计算代价高昂的过程。就此而言,throw 语句的性能特性是可以和 return 语句相媲美的。

用 throwing 函数传递错误

为了表示一个函数、方法或构造器可以抛出错误,在函数声明的参数之后加上 throws 关键字。一个标有 throws 关键字的函数被称作 throwing 函数。如果这个函数指明了返回值类型,throws 关键词需要写在返回箭头(->)的前面。

1
2
3
func canThrowErrors() throws -> String

func cannotThrowErrors() -> String

一个 throwing 函数可以在其内部抛出错误,并将错误传递到函数被调用时的作用域。

注意

只有 throwing 函数可以传递错误。任何在某个非 throwing 函数内部抛出的错误只能在函数内部处理。

下面的例子中,VendingMachine 类有一个 vend(itemNamed:) 方法,如果请求的物品不存在、缺货或者投入金额小于物品价格,该方法就会抛出一个相应的 VendingMachineError

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
struct Item {
var price: Int
var count: Int
}

class VendingMachine {
var inventory = [
"Candy Bar": Item(price: 12, count: 7),
"Chips": Item(price: 10, count: 4),
"Pretzels": Item(price: 7, count: 11)
]
var coinsDeposited = 0

func vend(itemNamed name: String) throws {
guard let item = inventory[name] else {
throw VendingMachineError.invalidSelection
}

guard item.count > 0 else {
throw VendingMachineError.outOfStock
}

guard item.price <= coinsDeposited else {
throw VendingMachineError.insufficientFunds(coinsNeeded: item.price - coinsDeposited)
}

coinsDeposited -= item.price

var newItem = item
newItem.count -= 1
inventory[name] = newItem

print("Dispensing \(name)")
}
}

vend(itemNamed:) 方法的实现中使用了 guard 语句来确保在购买某个物品所需的条件中有任一条件不满足时,能提前退出方法并抛出相应的错误。由于 throw 语句会立即退出方法,所以物品只有在所有条件都满足时才会被售出。

因为 vend(itemNamed:) 方法会传递出它抛出的任何错误,在你的代码中调用此方法的地方,必须要么直接处理这些错误——使用 do-catch 语句,try?try!;要么继续将这些错误传递下去。例如下面例子中,buyFavoriteSnack(person:vendingMachine:) 同样是一个 throwing 函数,任何由 vend(itemNamed:) 方法抛出的错误会一直被传递到 buyFavoriteSnack(person:vendingMachine:) 函数被调用的地方。

1
2
3
4
5
6
7
8
9
let favoriteSnacks = [
"Alice": "Chips",
"Bob": "Licorice",
"Eve": "Pretzels",
]
func buyFavoriteSnack(person: String, vendingMachine: VendingMachine) throws {
let snackName = favoriteSnacks[person] ?? "Candy Bar"
try vendingMachine.vend(itemNamed: snackName)
}

上例中,buyFavoriteSnack(person:vendingMachine:) 函数会查找某人最喜欢的零食,并通过调用 vend(itemNamed:) 方法来尝试为他们购买。因为 vend(itemNamed:) 方法能抛出错误,所以在调用的它时候在它前面加了 try 关键字。

throwing 构造器能像 throwing 函数一样传递错误。例如下面代码中的 PurchasedSnack 构造器在构造过程中调用了 throwing 函数,并且通过传递到它的调用者来处理这些错误。

1
2
3
4
5
6
7
struct PurchasedSnack {
let name: String
init(name: String, vendingMachine: VendingMachine) throws {
try vendingMachine.vend(itemNamed: name)
self.name = name
}
}

用 Do-Catch 处理错误

你可以使用一个 do-catch 语句运行一段闭包代码来处理错误。如果在 do 子句中的代码抛出了一个错误,这个错误会与 catch 子句做匹配,从而决定哪条子句能处理它。

下面是 do-catch 语句的一般形式:

1
2
3
4
5
6
7
8
9
10
do {
try expression
statements
} catch pattern 1 {
statements
} catch pattern 2 where condition {
statements
} catch {
statements
}

catch 后面写一个匹配模式来表明这个子句能处理什么样的错误。如果一条 catch 子句没有指定匹配模式,那么这条子句可以匹配任何错误,并且把错误绑定到一个名字为 error 的局部常量。关于模式匹配的更多信息请参考 模式

举例来说,下面的代码处理了 VendingMachineError 枚举类型的全部三种情况:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
var vendingMachine = VendingMachine()
vendingMachine.coinsDeposited = 8
do {
try buyFavoriteSnack(person: "Alice", vendingMachine: vendingMachine)
print("Success! Yum.")
} catch VendingMachineError.invalidSelection {
print("Invalid Selection.")
} catch VendingMachineError.outOfStock {
print("Out of Stock.")
} catch VendingMachineError.insufficientFunds(let coinsNeeded) {
print("Insufficient funds. Please insert an additional \(coinsNeeded) coins.")
} catch {
print("Unexpected error: \(error).")
}
// 打印“Insufficient funds. Please insert an additional 2 coins.”

上面的例子中,buyFavoriteSnack(person:vendingMachine:) 函数在一个 try 表达式中被调用,是因为它能抛出错误。如果错误被抛出,相应的执行会马上转移到 catch 子句中,并判断这个错误是否要被继续传递下去。如果错误没有被匹配,它会被最后一个 catch 语句捕获,并赋值给一个 error 常量。如果没有错误被抛出,do 子句中余下的语句就会被执行。

catch 子句不必将 do 子句中的代码所抛出的每一个可能的错误都作处理。如果所有 catch 子句都未处理错误,错误就会传递到周围的作用域。然而,错误还是必须要被某个周围的作用域处理的。在不会抛出错误的函数中,必须用 do-catch 语句处理错误。而能够抛出错误的函数既可以使用 do-catch 语句处理,也可以让调用方来处理错误。如果错误传递到了顶层作用域却依然没有被处理,你会得到一个运行时错误。

以下面的代码为例,不是 VendingMachineError 中申明的错误会在调用函数的地方被捕获:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
func nourish(with item: String) throws {
do {
try vendingMachine.vend(itemNamed: item)
} catch is VendingMachineError {
print("Invalid selection, out of stock, or not enough money.")
}
}

do {
try nourish(with: "Beet-Flavored Chips")
} catch {
print("Unexpected non-vending-machine-related error: \(error)")
}
// 打印“Invalid selection, out of stock, or not enough money.”

如果 vend(itemNamed:) 抛出的是一个 VendingMachineError 类型的错误,nourish(with:) 会打印一条消息,否则 nourish(with:) 会将错误抛给它的调用方。这个错误之后会被通用的 catch 语句捕获。

将错误转换成可选值

可以使用 try? 通过将错误转换成一个可选值来处理错误。如果是在计算 try? 表达式时抛出错误,该表达式的结果就为 nil。例如,在下面的代码中,xy 有着相同的数值和等价的含义:

1
2
3
4
5
6
7
8
9
10
11
12
func someThrowingFunction() throws -> Int {
// ...
}

let x = try? someThrowingFunction()

let y: Int?
do {
y = try someThrowingFunction()
} catch {
y = nil
}

如果 someThrowingFunction() 抛出一个错误,xy 的值是 nil。否则 xy 的值就是该函数的返回值。注意,无论 someThrowingFunction() 的返回值类型是什么类型,xy 都是这个类型的可选类型。例子中此函数返回一个整型,所以 xy 是可选整型。

如果你想对所有的错误都采用同样的方式来处理,用 try? 就可以让你写出简洁的错误处理代码。例如,下面的代码用几种方式来获取数据,如果所有方式都失败了则返回 nil

1
2
3
4
5
func fetchData() -> Data? {
if let data = try? fetchDataFromDisk() { return data }
if let data = try? fetchDataFromServer() { return data }
return nil
}

禁用错误传递

有时你知道某个 throwing 函数实际上在运行时是不会抛出错误的,在这种情况下,你可以在表达式前面写 try! 来禁用错误传递,这会把调用包装在一个不会有错误抛出的运行时断言中。如果真的抛出了错误,你会得到一个运行时错误。

例如,下面的代码使用了 loadImage(atPath:) 函数,该函数从给定的路径加载图片资源,如果图片无法载入则抛出一个错误。在这种情况下,因为图片是和应用绑定的,运行时不会有错误抛出,所以适合禁用错误传递。

1
let photo = try! loadImage(atPath: "./Resources/John Appleseed.jpg")

指定清理操作

你可以使用 defer 语句在即将离开当前代码块时执行一系列语句。该语句让你能执行一些必要的清理工作,不管是以何种方式离开当前代码块的——无论是由于抛出错误而离开,或是由于诸如 returnbreak 的语句。例如,你可以用 defer 语句来确保文件描述符得以关闭,以及手动分配的内存得以释放。

defer 语句将代码的执行延迟到当前的作用域退出之前。该语句由 defer 关键字和要被延迟执行的语句组成。延迟执行的语句不能包含任何控制转移语句,例如 breakreturn 语句,或是抛出一个错误。延迟执行的操作会按照它们声明的顺序从后往前执行——也就是说,第一条 defer 语句中的代码最后才执行,第二条 defer 语句中的代码倒数第二个执行,以此类推。最后一条语句会第一个执行。

1
2
3
4
5
6
7
8
9
10
11
12
func processFile(filename: String) throws {
if exists(filename) {
let file = open(filename)
defer {
close(file)
}
while let line = try file.readline() {
// 处理文件。
}
// close(file) 会在这里被调用,即作用域的最后。
}
}

上面的代码使用一条 defer 语句来确保 open(_:) 函数有一个相应的对 close(_:) 函数的调用。

注意

即使没有涉及到错误处理的代码,你也可以使用 defer 语句。

留言與分享

作者的圖片

Kein Chan

這是獨立全棧工程師Kein Chan的技術博客
分享一些技術教程,命令備忘(cheat-sheet)等


全棧工程師
資深技術顧問
數據科學家
Hit廣島觀光大使


Tokyo/Macau