检查协议一致性
你可以使⽤《类型转换》中描述的 is 和 as 操作符来检查协议一致性,即是否符合某协议,并且可以转换到指定的协议类型。检
查和转换协议的语法与检查和转换类型是完全一样的:
1. is 用来检查实例是否符合某个协议,若符合则返回 true ,否则返回 false ;
2. as? 返回一个可选值,当实例符合某个协议时,返回类型为协议类型的可选值,否则返回 nil ;
3. as! 将实例强制向下转换到某个协议类型,如果强转失败,将触发运⾏时错误。
下面的例子定义了一个 HasArea 协议,该协议定义了一个 Double 类型的可读属性 area :
protocol HasArea {
var area: Double { get }
}
如下所示, Circle 类和 Country 类都遵循了 HasArea 协议:
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 协议的类:
class Animal {
var legs: Int
init(legs: Int) { self.legs = legs }
}
Circle , Country , Animal 并没有一个共同的基类,尽管如此,它们都是类,它们的实例都可以作为AnyObject 类型的值,存储
在同一个数组中:
let objects: [AnyObject] = [
Circle(radius: 2.0),
Country(area: 243_610),
Animal(legs: 4)
]
objects 数组使用字面量初始化,数组包含一个 radius 为 2 的 Circle 的实例,一个保存了英国土面积的 Country 实例和一个 legs
为 4 的 Animal 实例。
如下所示, objects 数组可以被迭代,并对迭代出的每一个元素进⾏检查,看它是否符合 HasArea 协议:
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 常量上。
objectWithArea 是 HasArea 协议类型的实例,因此 area 属性可以被访问和打印。
objects 数组中的元素的类型并不会因为强转而丢失类型信息,它们仍然是 Circle , Country , Animal 类型。 然而,当它们被
赋值给 objectWithArea 常量时,只被视为 HasArea 类型,因此只有 area 属性能够被访问。
可选的协议要求
协议可以定义可选要求,遵循协议的类型可以选择是否实现这些要求。在协议中使用 optional 关键字作为前缀来定义可选要求。
可选要求用在你需要和 Objective-C 打交道的代码中。协议和可选要求都必须带上 @objc 属性。标记 @objc 特性的协议只能被
继承自 Objective-C 类的类或者 @objc 类遵循,其他类以及结构体和枚举均不能遵循这种协议。
使用可选要求时(例如,可选的方法或者属性),它们的类型会自动变成可选的。⽐如,一个类型为 (Int) -> String 的方法会变成
((Int) -> String)? 。需要注意的是整个函数类型是可选的,⽽不是函数的返回值。
协议中的可选要求可通过可选链式调用来使用,因为遵循协议的类型可能没有实现这些可选要求。类似 someOptionalMethod?
(someArgument) 这样,你可以在可选方法名称后加上 ? 来调用可选方法。详细内容可在《可选链式调用》章节中查看。
下⾯的例子定义了一个名为 Counter 的用于整数计数的类,它使用外部的数据源来提供每次的增量。数据源 CounterDataSource
协议定义,它包含两个可选要求:
@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 ,如下所示:
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:) 方法获取到值,可能由于 dataSource 为 nil ,或者它并没有实现 increment(forCount:) 方法,
那么 increment() 方法将试图从数据源的 fixedIncrement 属性中获取增量。 fixedIncrement 是一个可选属性,因此属性值是一个
Int? 值,即使该属性在 CounterDataSource 协议中的类型是非可选的 Int 。
下⾯的例子展示了 CounterDataSource 的简单实现。 ThreeSource 类遵循了 CounterDataSource 协议,它实现了可选属性
fixedIncrement ,每次会返回 3 :
class ThreeSource: NSObject, CounterDataSource {
let fixedIncrement = 3
}
可以使用 ThreeSource 的实例作为 Counter 实例的数据源:
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 :
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 ,数值便不会再有变动:
counter.count = -4
counter.dataSource = TowardsZeroSource()
for _ in 1...5 {
counter.increment()
print(counter.count)
}
// -3
// -2
// -1
// 0
// 0
协议扩展
协议可以通过扩展来为遵循协议的类型提供属性、方法以及下标的实现。通过这种方式,你可以基于协议本身来实现这些功能,⽽
无需在每个遵循协议的类型中都重复同样的实现,也⽆需使用全局函数。
例如,可以扩展 RandomNumberGenerator 协议来提供 randomBool() ⽅法。该方法使用协议中定义的 random()方法来返回一个
随机的 Bool 值:
extension RandomNumberGenerator {
func randomBool() -> Bool {
return random() > 0.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 属性的值:
extension PrettyTextRepresentable {
var prettyTextualDescription: String {
return textualDescription
}
}
为协议扩展添加限制条件
在扩展协议的时候,可以指定一些限制条件,只有遵循协议的类型满足这些限制条件时,才能获得协议扩展提供的默认实现。这
些限制条件写在协议名之后,使用 where 子句来描述,正如《泛型 Where ⼦句》中所描述的。
例如,你可以扩展 Collection 协议,适用于集合中的元素遵循了 Equatable 协议的情况。通过限制集合元素遵 Equatable 协议,
作为标准库的一部分, 你可以使用 == 和 != 操作符来检查两个元素的等价性和非等价性。
extension Collection where Element: Equatable {
func allEqual() -> Bool {
for element in self {
if element != self.first {
return false
}
}
return true
}
}
如果集合中的所有元素都一致, allEqual() 方法才返回 true 。 看看两个整数数组,一个数组的所有元素都是一样的,另一个不一
样:
let equalNumbers = [100, 100, 100, 100, 100]
let differentNumbers = [100, 100, 200, 100, 200]
由于数组遵循 Collection 而且整数遵循 Equatable , equalNumbers 和 differentNumbers 都可以使用allEqual() 方法。
print(equalNumbers.allEqual())
// 打印 "true"
print(differentNumbers.allEqual())
// 打印 "false"
注意
如果一个遵循的类型满⾜了为同一方法或属性提供实现的多个限制型扩展的要求, Swift 会使用最匹配限制的实现。