23.17. 可选的协议要求

协议可以定义可选要求,遵循协议的类型可以选择是否实现这些要求。在协议中使用 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。另外,即使这里使用了两层可选链式调用,最后的返回结果依旧是单层的可选类型。关于这一点的更多信息,请查阅 17.6. 连接多层可选链式调用

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

如果没有从 increment(forCount:) 方法获取到值,可能由于 dataSourcenil,或者它并没有实现 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