原文来自Raywenderlich,作者Eric Kerber,原文链接:
https://www.raywenderlich.com/109156/introducing-protocol-oriented-programming-in-swift-2
Note: 本教程需要Xcode7以及Swift2,可以在Apple官方网站下载(译者注:原文发布是Swift2还处于beta阶段)。
在WWDC2015时,Apple发布了Swift语言的第二个大的版本更新——Swift 2。新版本包含了很多新的语言特性(译者注:中文改动可以看这里,不过推荐阅读英文版改动日志),让Swift在使用的时候更加得心应手。
众多改动之中,最引人注意的就是protocol extensions。在Swift第一版中,我们可以通过extension来为已有的class,struct或enum拓展功能。而在Swift 2中,我们也可以为protocol添加extension。
可能一开始看上去这个新特性并不起眼,实际上protocol extensions非常强大,以至于可以改变Swift之前的某些编程思想。在本教程中,我们会展示如何创建并使用protocol extensions,以及随之而来的一些新技巧和面向接口编程(Protocol-oriented programming,以下简称POP)模式对代码的影响。
你将会了解Swift团队是如何使用protocol extensions来改善Swift标准库,以及如何使用这些技巧来改善自己的代码。
准备工作
首先,创建一个新的playground。在Xcode中,选择File\New\Playground...,然后将playground命名为SwiftProtocols。platform可以选择任意选项,之后我们要构建的代码都是平台无关的。点击Next选择文件路径,最后点击Create。
新的playground打开之后,加入如下代码:
protocol Bird {
var name: String { get }
var canFly: Bool { get }
}
protocol Flyable {
var airspeedVelocity: Double { get }
}
以上代码定义了一个简单的protocol——Bird。它有name和canFly两个属性。还有一个protocol叫做Flyable,定义了属性airspeedVelocity。
之前,你或许会把Flyable定义成一个基类(base class),然后利用继承的方法来定义Birds以及其它“会飞”的类型,例如飞机(译者注:Flyable意为飞行物,Birds就是鸟)。而从现在开始,所有的东西都开始被定义为protocol了!
当我们开始定义具体的类型的时候,你就会发现,这一切会让整个系统变得多么灵活。
定义Protocol-conforming类型
将下面定义struct的代码添加到playground中:
struct FlappyBird: Bird, Flyable {
let name: String
let flappyAmplitude: Double
let flappyFrequency: Double
let canFly = true
var airspeedVelocity: Double {
return 3 * flappyFrequency * flappyAmplitude
}
}
这里定义了一个新的struct类型——FlappyBird。它同时满足Bird和Flyable两个protocol。airspeedVelocity作为一个computed property,由flappyFrequency和flappyAmplitude计算得出。作为“会飞的鸟”,FlappyBird将canFly设置为true。
接下来,把下面定义两个struct的代码加到playground中(译者注:Penguin是企鹅,SwiftBird可以理解为速度很快的鸟):
struct Penguin: Bird {
let name: String
let canFly = false
}
struct SwiftBird: Bird, Flyable {
var name: String { return "Swift \(version)" }
let version: Double
let canFly = true
// Swift is FAST!
var airspeedVelocity: Double { return 2000.0 }
}
Penguin与Bird是is-a关系,但并不能飞。Aha——幸亏没有使用继承的方式,否则所有的Bird都会被定义为Flyable。对于SwiftBird,自然而然的,它速度很快,飞行速度的数值也很高。
然而,或许你已经注意到了,目前的实现还是有一些冗余存在。每一种Bird类型必须声明canFly属性,不管它到底会不会飞。我们的系统中已经有了Flyable的概念,却并没能很好的利用起来。
用默认实现来拓展Protocols
利用protocol extensions,我们可以定义protocol的默认行为。添加下面的代码到playground中:
extension Bird where Self: Flyable {
// Flyable birds can fly!
var canFly: Bool { return true }
}
这段代码定义了一个Bird的extension, 并设置了canFly属性的默认行为。当实现Bird protocol的对象同时实现了Flyable protocol的时候,让canFly的值总是返回true。换句话说,“会飞的鸟”终于不用再显式的声明自己“会飞”了!
Swift 1.2 引入了与if-let关联的where语法,Swift 2则把where进一步用于了有条件的拓展protocol。
删除FlappyBird和SwiftBird定义里面的let canFly = true。你会发现playground依然成功build,因为protocol extension帮你处理了冗余的部分。
为什么不使用基类(Base Classes)?
Protocol extensions和默认行为(Default Behavior)可能看起来和使用基类很相似。更确切一点,很像其它一些语言中的abstract class。但在Swift中,protocol extensions主要有如下一些便利:
- 因为类型可以同时满足超过一个protocol,我们可以用多个不同的protocol的默认行为来构建这个类型对象。不同于某些语言支持的多重继承(multiple inheritance),protocol extensions并不会引入额外的状态。
- Protocols可以用于类,结构体和枚举类型(classes,structs and enums),而基类和继承只能用于类类型(class types)。
换句话说,protocol extensions为值类型(value types)提供了定义默认行为的方法。
展示了struct类型的例子之后,让我们把下面的enum定义加到playground中:
enum UnladenSwallow: Bird, Flyable {
case African
case European
case Unknown
var name: String {
switch self {
case .African:
return "African"
case .European:
return "European"
case .Unknown:
return "What do you mean? African or European?"
}
}
var airspeedVelocity: Double {
switch self {
case .African:
return 10.0
case .European:
return 9.9
case .Unknown:
fatalError("You are thrown from the bridge of death!")
}
}
}
如同其它值类型一样,你需要做的只是定义正确的属性来让UnladenSwallow满足两个protocol即可(译者注:UnladenSwallow当成是一种燕子就行)。因为它同时满足Bird和Flyable,所以直接拥有canFly的默认实现。
你难道觉得这个用了airspeedVelocity的教程不会提到Monty Python么?:](译者注:貌似是作者用了一个梗,Monty Python是国外一个著名的喜剧团体)。
Protocols的扩展
大概protocol extensions最常见的用途就是用来扩展外部的protocols了,往往要么是来自Swift标准库,要么是来自第三方库。
把下面的代码加入playground:
extension CollectionType {
func skip(skip: Int) -> [Generator.Element] {
guard skip != 0 else { return [] }
var index = self.startIndex
var result: [Generator.Element] = []
var i = 0
repeat {
if i % skip == 0 {
result.append(self[index])
}
index = index.successor()
i++
} while (index != self.endIndex)
return result
}
}
这段代码定义了一个CollectionType的extension,其中包含了一个新的skip(_:)方法。这个方法会遍历collection中的内容并每次“跳过”特定个数的元素,最终返回那些没有被“跳过”的元素的collection。
CollectionType是一个protocol。Swift中一些例如数组(arrays)和字典(Dictionaries)都实现了这个protocol。这意味着现在skip(_:)这个新方法适用于所有的CollectionType。
扩展自定义的Protocols
给Swift标准库添加新行为让人兴奋。与此同时,我们也可以利用标准库的protocol extensions给自定义的类型定义默认行为。
修改Bird protocol使得它满足BooleanType protocol:
protocol Bird: BooleanType {
满足BooleanType意味着这个类型要拥有一个booleanValue属性,让它能够像一个布尔值一样被使用。这是不是表示我们要给每个Bird类型都添加这个属性呢?
当然不需要,有更简单的方法。添加如下代码到Bird的定义中:
extension BooleanType where Self: Bird {
var boolValue: Bool {
return self.canFly
}
}
这个extension会让canFly属性代表每个Bird类型的布尔值。
为了测试一下效果,添加如下代码到playground的末尾:
if UnladenSwallow.African {
print("I can fly!")
} else {
print("Guess I’ll just sit here :[")
}
你应该看到“I can fly”出现在了辅助编辑器上。但更加值得注意的是,你刚刚在if条件语句中使用了African Unladen Swallow!
对Swift标准库的影响
你已经了解了protocol extensions在拓展自定义以及外部代码功能方面是多么好用。而Swift团队用protocol extensions来改善Swift标准库的方法可能会更加让你感到惊奇。
Swift相当提倡函数式编程,在标准库中引入了很多范例,包括map,reduce和filter等。这些方法被应用在很多CollectionType中,比如Array:
// 计算数组中有多少个字符
["frog", "pants"].map { $0.length }.reduce(0) { $0 + $1 } // 返回 9
对数组调用map方法会返回另一个数组,而reduce则将结果归约成一个最终值9。
在这个例子中,map和reduce都是Swift标准库的一部分。如果你按下cmd同时点击map,你可以看到具体的方法定义。
在Swift 1.2中,定义看起来是这样的:
// Swift 1.2
extension Array : _ArrayType {
/// 返回一个 `Array`, 其中包含调用的结果
/// `transform(x)` 作用于每个`self`的`x`元素
func map<U>(transform: (T) -> U) -> [U]
}
这表明在Swift 1.2中,map需要在每个Swift标准库的CollectionType中被重定义!这是因为即使Array和Range都是CollectionType,结构体(struct)不能有子类,也不能有共享的方法实现。
这不仅仅是Swift标准库的实现细节,这为我们使用Swift类型增加了限制。
下面的泛型方法接受一个Flyable的CollectionType,返回其中airspeedVelocity最高的元素。
func topSpeed<T: CollectionType where T.GeneratorType: Flyable (collection: T) -> Double {
collection.map { $0.airspeedVelocity }.reduce { max($0, $1)}
}
在Swift 1.2中,没有protocol extensions,这实际上会带来编译错误。map和reduce只存在于定义好的具体类型当中,对于任意的CollectionType是没有效果的。
在Swift 2中,得益于protocol extensions,map的定义变成了如下:
// Swift 2.0
extension CollectionType {
/// 返回一个 `Array`,包含把 `transform`映射到
/// `self`上的结果.
///
/// - 复杂度: O(N).
func map<T>(@noescape transform: (Self.Generator.Element) -> T) -> [T]
}
即使你在Swift开源之前暂时不能看到源代码(译者注:现在已经开源),所有的CollectionType现在都拥有了map的默认实现!
将下面之前提到过的泛型方法加入到playground中:
func topSpeed<T: CollectionType where T.Generator.Element == Flyable>(c: T) -> Double {
return c.map { $0.airspeedVelocity }.reduce(0) { max($0, $1) }
}
map和reduce方法现在可以用于你的Flyable集合实例了。把下面的代码加入playground,你现在终于可以回答它们之中谁是最快的了:
let flyingBirds: [Flyable] =
[UnladenSwallow.African,
UnladenSwallow.European,
SwiftBird(version: 2.0)]
topSpeed(flyingBirds) // 2000.0
结果毫无疑问,不是么?:]
下一步该做什么
可以在这里下载本教程完整的playground代码。
通过搭建简单的protocols并用protocol extensions拓展它们,你已经见识过了POP的强大之处。使用默认实现,你可以让已有的protocols拥有简明且自动化的行为,类似基类但胜在能应用于struct和enum类型。
更进一步的,protocol extensions不仅能扩展自定义的protocols,还可以扩展Swift标准库,Cocoa以及Cocoa Touch中的protocols。
想要对Swift 2的新特性进行总览的话,可以阅读我们的what's new in Swift 2.0,或者移步官方博客Apple’s Swift blog。
Apple’s developer portal上有个很棒的WWDC视频推荐:Protocol Oriented Programming。想要深度了解POP理论的同学可以看看。
如果还有什么问题,欢迎在原文以及这里的评论区进行讨论。
写在最后:本文中一些不影响文章理解的专业名词没有进行翻译,比如protocol和extensions,我尽量避免使用诸如“协议拓展”之类的字眼,一是避免和大家头脑中的中文版本产生偏差,二是对于这些词汇尽量标准化比较好。POP是翻译成“面向接口编程”还是“面向协议编程”纠结了很久,还是感觉“接口”用起来更容易被理解。第一次翻译技术文章,希望大家能提出一些建设性意见:-)