Swift协议初识
Protocol
Swift 标准库中有 50 多个复杂不一的协议,几乎所有的实际类型都是满足若干协议的。protocol 是 Swift 语言的底座,语言的其他部分正是在这个底座上组织和建立起来的。
所谓协议,就是一组属性和/或方法的定义,而如果某个具体类型想要遵守一个协议,那它需要实现这个协议所定义的所有这些内容。协议实际上做的事情不过是“关于实现的约定”。
面向对象
class Animal {
var leg: Int { return 2 }
func eat() {
print("eat food.")
}
func run() {
print("run with \(leg) legs")
}
}
class Tiger: Animal {
override var leg: Int { return 4 }
override func eat() {
print("eat meat.")
}
}
let tiger = Tiger()
tiger.eat() // "eat meat"
tiger.run() // "run with 4 legs"
我们看到 Tiger
和 Animal
共享了一部分代码,这部分代码被封装到了父类中,而除了 Tiger
的其他的子类也能够使用 Animal
的这些代码。这其实就是 OOP 的核心思想 - 使用封装和继承,将一系列相关的内容放到一起。
这套理念有一些缺陷。虽然我们努力用这套抽象和继承的方法进行建模,但是实际的事物往往是一系列特质的组合,而不单单是以一脉相承并逐渐扩展的方式构建的。
下面有几个解决方案:
-
Copy & Paste
这是一个比较糟糕的解决方案,但是演讲现场还是有不少朋友选择了这个方案,特别是在工期很紧,无暇优化的情况下。这诚然可以理解,但是这也是坏代码的开头。我们应该尽量避免这种做法。
-
引入 BaseViewController
在一个继承自
UIViewController
的BaseViewController
上添加需要共享的代码,或者干脆在UIViewController
上添加 extension。看起来这是一个稍微靠谱的做法,但是如果不断这么做,会让所谓的Base
很快变成垃圾堆。职责不明确,任何东西都能扔进Base
,你完全不知道哪些类走了Base
,而这个“超级类”对代码的影响也会不可预估。 -
依赖注入
通过外界传入一个带有
myMethod
的对象,用新的类型来提供这个功能。这是一个稍好的方式,但是引入额外的依赖关系,可能也是我们不太愿意看到的。 -
多继承
当然,Swift 是不支持多继承的。不过如果有多继承的话,我们确实可以从多个父类进行继承,并将
myMethod
添加到合适的地方。有一些语言选择了支持多继承 (比如 C++),但是它会带来 OOP 中另一个著名的问题:菱形缺陷。菱形缺陷
上面的例子中,如果我们有多继承,那么
ViewController
和AnotherViewController
的关系可能会是这样的:在上面这种拓扑结构中,我们只需要在
ViewController
中实现myMethod
,在AnotherViewController
中也就可以继承并使用它了。看起来很完美,我们避免了重复。但是多继承有一个无法回避的问题,就是两个父类都实现了同样的方法时,子类该怎么办?我们很难确定应该继承哪一个父类的方法。因为多继承的拓扑结构是一个菱形,所以这个问题又被叫做菱形缺陷 (Diamond Problem)。像是 C++ 这样的语言选择粗暴地将菱形缺陷的问题交给程序员处理,这无疑非常复杂,并且增加了人为错误的可能性。而绝大多数现代语言对多继承这个特性选择避而远之。
动态派发安全性
ViewController *v1 = ...
[v1 myMethod];
AnotherViewController *v2 = ...
[v2 myMethod];
NSObject *v3 = [NSObject new]
// v3 没有实现 `myMethod`
NSArray *array = @[v1, v2, v3];
for (id obj in array) {
[obj myMethod];
}
// Runtime error:
// unrecognized selector sent to instance blabla
编译依然可以通过,但是显然,程序将在运行时崩溃。Objective-C 是不安全的,编译器默认你知道某个方法确实有实现,这是消息发送的灵活性所必须付出的代价。而在 app 开发看来,用可能的崩溃来换取灵活性,显然这个代价太大了。虽然这不是 OOP 范式的问题,但它确实在 Objective-C 时代给我们带来了切肤之痛。
OOP的三大困境
- 动态派发安全
- 横切关注点
- 菱形缺陷
首先,在 OC 中动态派发让我们承担了在运行时才发现错误的风险,这很有可能是发生在上线产品中的错误。其次,横切关注点让我们难以对对象进行完美的建模,代码的重用也会更加糟糕。
协议拓展和面向协议编程
-
[x] 动态派发安全
```swift protocol Greetable { var name: String { get } func greet() } struct Person: Greetable { let name: String func greet() { print("你好 \(name)") } } struct Cat: Greetable { let name: String func greet() { print("meow~ \(name)") } } //以协议作为类型 //实现动态派发 let array: [Greetable] = [ Person(name: "Wei Wang"), Cat(name: "onevcat")] for obj in array { obj.greet() } //❌ struct Bug: Greetable { let name: String } // Compiler Error: // 'Bug' does not conform to protocol 'Greetable' // protocol requires function 'greet()' ```
-
[ ] 横切关注点
配合协议拓展实现 > 协议拓展 Swift2 实现 WWDC2015提出 ```swift protocol P { func myMethod() } //拓展协议 P extension P //提供默认实现 func myMethod() { doWork() } } extension ViewController: P { } extension AnotherViewController: P { } viewController.myMethod() anotherViewController.myMethod() ``` 总结: - 协议定义 - 提供实现的入口 - 遵循协议的类型需要对其进行实现 - 协议扩展 - 为入口提供默认实现 - 根据入口提供额外实现
-
[ ] 菱形缺陷
```swift protocol Nameable { var name: String { get } } protocol Identifiable { var name: String { get } var id: Int { get } } struct Person: Nameable, Identifiable { let name: String let id: Int } // `name` 属性同时满足 Nameable 和 Identifiable 的 name ``` 如果对协议添加了默认实现 ```swift extension Nameable { var name: String { return "default name" } } struct Person: Nameable, Identifiable { //name 使用了 Nameable 的 name 属性 //let name: String let id: Int } ``` 这样的编译是可以通过的,虽然 `Person` 中没有定义 `name`,但是通过 `Nameable` 的 `name` (因为它是静态派发的),`Person` 依然可以遵守 `Identifiable`。不过,当 `Nameable` 和 `Identifiable` 都有 `name` 的协议扩展的话,就无法编译了: ```swift extension Nameable { var name: String { return "default name" } } extension Identifiable { var name: String { return "another default name" } } struct Person: Nameable, Identifiable { // let name: String let id: Int } // 无法编译,name 属性冲突 ``` 这种情况下,`Person` 无法确定要使用哪个协议扩展中 `name` 的定义。在同时实现两个含有同名元素的协议,**并且**它们都提供了默认扩展时,我们需要在**具体的类型中明确地提供实现**。这里我们将 `Person` 中的 `name` 进行实现就可以了: ```swift struct Person: Nameable, Identifiable { let name: String let id: Int } Person(name: "onevcat", id: 123).name // onevcat ``` 这里的行为看起来和菱形问题很像,但是有一些本质不同。首先,这个问题出现的前提条件是同名元素**以及**同时提供了实现,而协议扩展对于协议本身来说并不是必须的。其次,我们在具体类型中提供的实现一定是安全和确定的。当然,菱形缺陷没有被完全解决,**Swift 还不能很好地处理多个协议的冲突**,这是 Swift 现在的不足。