随 Xcode 9.3 发布的 Swift 4.1 带来了诸多新特性,其中一项特性称为 Conditional Conformance. 有不少博主都在第一时间写文章介绍了这个特性是如何使用的,然而读完几篇之后我还是有点一头雾水,不知道为何它叫这个名字,有什么限制条件,用在什么情况之下。因此,这篇文章记录了我的学习理解过程。

定义

其实苹果很早就在 swift-evolution 仓库里发布过这个提案:Conditional Conformance. 我想很多人跟我一样,读完第一段的 Introduction 就明白了:

Conditional conformances express the notion that a generic type will conform to a particular protocol only when its type arguments meet certain requirements. For example, the Array collection can implement the Equatable protocol only when its elements are themselves Equatable, which can be expressed via the following conditional conformance on Equatable:

extension Array: Equatable where Element: Equatable {
  static func ==(lhs: Array<Element>, rhs: Array<Element>) -> Bool { ... }
}

也就是说:对于一个泛型类型来说,只有当它的类型参数满足特性条件的情况下(conditional),它才会遵循某个协议(conformance)。对于上面这个数组类型的例子来说,当它的元素类型(类型参数)遵循 Equatable 协议时,数组也遵循 Equatable 协议。也就是说,当数组元素可比较时,数组也可以比较。

如果只看上面这个例子,可能会给人一种误解:类型参数需要遵循的协议和泛型类型遵循的协议是同一个协议。但实际上这个特性并没有这种限制,例如:

protocol Testable {
    func testMe()
}

protocol Arratable {
    func array()
}

extension Array: Arratable where Element: Testable {
    func array() {
        print("I'm an array")
    }
}

struct TestElement: Testable {
    func testMe() {
        // do something
    }
}

let element = TestElement()
let array = [element]
array.array()

这里声明了两个协议:TestableArratable,并且给Array定义了一个 extension:当它的元素类型遵循Testable协议时,Array遵循Arratable协议。

运行时检查

Conditional Conformance也支持运行时检查功能:

protocol P {
  func doSomething()
}

struct S: P {
  func doSomething() { print("S") }
}

// Array conforms to P if it's element type conforms to P
extension Array: P where Element: P {
  func doSomething() {
    for value in self {
      value.doSomething()
    }
  }
}

// Dynamically query and use conformance to P.
func doSomethingIfP(_ value: Any) {
  if let p = value as? P {
    p.doSomething()
  } else {
    print("Not a P")
  }
}

doSomethingIfP([S(), S(), S()]) // prints "S" three times
doSomethingIfP([1, 2, 3])       // prints "Not a P"

调用doSomethingIfP时,if-let会检查value是否遵循协议P,当value是一个Array时,因为这个特性的存在,runtime 会继续检查Array的元素类型是否遵循协议P,因此上面代码中第一个调用会打印出来 3 个S

隐式遵循

对于普通的协议遵循(non-conditional conformance)来说,如果一个类型遵循了某个协议,那么同时它也遵循了这个协议所继承的协议。例如,如果一个类型遵循了Collection协议,那么它也同时遵循了Sequence协议。然而对于条件遵循来说,你需要显示的声明这个类型遵循某个协议:

protocol P { 
  func checkValue() -> Bool
}
protocol Q : P { }
protocol R : P { }

struct X<T> { }

extension X: Q where T: Q { }
extension X: R where T: R { }

// error: X does not conform to protocol P; add
//
//   extension X: P where <#constraints#> { ... }
//
// to state conformance to P.

也就是说,尽管结构体 X 遵循 Q 和 R 协议,并且 Q 和 R 协议继承自 P 协议,但是 X 并不会隐式的自动遵循 P 协议,你需要手动定义 X 遵循 P 协议。这是因为,X 既遵循 Q 也遵循 R 协议,所以如果 X 自动遵循 P 协议的话,它无法知道应该应用 Q 或者 R 中的哪一个方法。比如说上面的代码,如果 Q 的 checkValue 返回 true,R 的 checkValue 返回 false,很显然,X 是无法同时满足这一情况的。

标准库支持

目前,标准库中已经有很多类型添加了对 Conditional Conformance 的支持:

// Equatable
extension Optional: Equatable where Wrapped: Equatable { /*== already exists */ }
extension Array: Equatable where Element: Equatable { /*== already exists */ }
extension ArraySlice: Equatable where Element: Equatable { /*== already exists */ }
extension ContiguousArray: Equatable where Element: Equatable { /*== already exists */ }
extension Dictionary: Equatable where Value: Equatable { /*== already exists */ }

// Hashable
extension Optional: Hashable where Wrapped: Hashable { /*...*/ }
extension Array: Hashable where Element: Hashable { /*...*/ }
extension ArraySlice: Hashable where Element: Hashable { /*...*/ }
extension ContiguousArray: Hashable where Element: Hashable { /*...*/ }
extension Dictionary: Hashable where Value: Hashable { /*...*/ }
extension Range: Hashable where Bound: Hashable { /*...*/ }
extension ClosedRange: Hashable where Bound: Hashable { /*...*/ }

并且,由于增加了这个特性,标准库中很多类型的变种也可以通过Conditional Conformance而不是用增加新类型的方式来实现。例如

ReversedCollection<Base: BidirectionalCollection>: BidirectionalCollection

以及

ReversedRandomAccessCollection<Base: RandomAccessCollection>: RandomAccessCollection

通过Conditional Conformance,你只需要定义一个 extension 就可以实现:

extension ReversedCollection: RandomAccessCollection where Base: RandomAccessCollection { }

@available(*, deprecated, renamed: "ReversedCollection")
public typealias ReversedRandomAccessCollection<T: RandomAccessCollection> = ReversedCollection<T>

总结

这个特性的增加使得 Swift 可以将之前大量的“重复”代码从标准库里移除,对于我们 Swift 应用开发来说,用好这个特性也可以帮主我们自己减少大量重复代码。此外,它还增强了 Swift 的泛型特性,对于即将到来的 ABI stability 也有推进作用。