本文翻譯自:Ray Wenderlich
原文地址:Introducing Protocol-Oriented Programming in Swift 2

說明:本教程使用了最新的 beta 版 Xcode 7 與 Swift 2.0,它們的正式版將會在今年的秋季發(fā)布。目前可以到蘋果的開發(fā)者官網(wǎng)上下載最新的 beta 版 Xcode。
在 WWDC 2015上,蘋果發(fā)布了 Swift 2.0 版本,這是自 Swift 發(fā)布以來進行的第二次重大改版,這次改進推出了很多新特性來幫助我等程序員寫出更優(yōu)雅的代碼。
在這些新特性里面,最讓人興奮的莫過于 協(xié)議擴展(protocol extensions) 了。在第一版的 Swift 當中,我們可以使用擴展來為 類(class)、結構體(struct) 以及 枚舉(enum) 增加新功能。在新版的 Swift 2.0 當中,我們也可以對 協(xié)議(protocol) 進行擴展了。
這乍看起來好像只是一個很小的改變,事實上,協(xié)議擴展的功能是相當強大的,它甚至能改變我們編寫代碼的方式。在本教程中,你不僅可以學習到創(chuàng)建和使用協(xié)議擴展的方法,同時還可以體會到這項新技術和面向協(xié)議編程范式給你帶來的新視野。
同時我們還能看到 Swift 開發(fā)小組是如何使用協(xié)議擴展來對 Swift 的標準庫進行改進,以及它將會對我們編寫代碼帶來怎樣的影響。
從這里開始(Getting Started)
我們先從新建一個 playground開始。打開 Xcode 7 選擇 File\New\Playground... 并命名為 SwiftProtocols??梢赃x擇任意的平臺,因為本教程中的代碼是與平臺無關的。選擇保存的位置后點擊 Next,最后點擊 Create。
打開 playground 后,添加如下代碼:
protocol Bird {
var name: String { get }
var canFly: Bool { get }
}
protocol Flyable {
var airspeedVelocity: Double { get }
}
這里定義了一個簡單的 Bird 協(xié)議,它擁有 name 和 canFly 兩個屬性,同時還定義了一個擁有 airspeedVelocity 屬性的 Flyable 協(xié)議。
在沒有協(xié)議的遠古時期,我們可能會將 Flyable 定義為一個基類,然后使用繼承的方式來定義 Bird 以及其它會飛的東西,比如飛機之類的。然而這里并不用這么做,這里的所有一切都是從協(xié)議開始的。
當我們接下來開始定義具體類型的時候,你將會看到這種方法將會使我們的整個系統(tǒng)更加靈活。
定義遵守協(xié)議的類型
在代碼區(qū)底部增加如下的 結構體 定義:
struct FlappyBird: Bird, Flyable {
let name: String
let flappyAmplitude: Double
let flappyFrequency: Double
let canFly = true
var airspeedVelocity: Double {
return 3 * flappyFrequency * flappyAmplitude
}
}
這里定義了一個新的結構體 Flappybird,這個結構體遵守了 Bird 和 Flyable 協(xié)議。它的 airspeedVelocity 屬性的值是通過 flappyFrequency 和 flappyAmplitue 計算出來的。作為一只像素鳥,它的 canFly 當然是返回 true的 :]。
接著,再定義兩個結構?體:
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 速度超群!
var airspeedVelocity: Double { return 2000.0 }
}
企鵝(Penguin)是一種鳥(Bird),不過它是不能飛的。啊哈,這時應該慶幸我們沒有采用繼承的方法,使用繼承會讓所有的子類都擁有飛的能力。雨燕鳥(SwiftBird)不僅能飛,它還擁有超快的速度!
我們可以發(fā)現(xiàn),上面的代碼里面有一些冗余。盡管我們已經(jīng)有了 Flyable 的信息,但是我們還是得為每個 Bird 類型指定 canFly 屬性來表明它是否可以飛行。
擁有默認實現(xiàn)的擴展協(xié)議
對于協(xié)議擴展,我們可以為它指定默認的實現(xiàn)。在定義 Bird 協(xié)議的下方增加如下代碼:
extension Bird where Self: Flyable {
// Flyable birds can fly!
var canFly: Bool { return true }
}
這里通過對 Bird 協(xié)議進行擴展,為它增加了默認行為。當一個類同時遵守 Bird 和 Flyable 協(xié)議時,它的 canFly 屬性就默認返回 true。即是說,所有遵守 Flyable 協(xié)議的鳥類都不必再顯式指定它是否可以飛行了。
Swift 1.2 將 where 判斷語法增加到了 if-let 綁定中,而 Swift 2.0 更進一步地將這個語法帶到了協(xié)議擴展中。
將 FlappyBird 和 SwiftBird 結構體定義中的 let canFly = true 語句刪除??梢钥吹剑?code>playground 可以順利通過編譯,因為擴展協(xié)議的默認實現(xiàn)已經(jīng)幫你處理了這些瑣事。
為何不使用基類?
或許你會發(fā)現(xiàn),使用協(xié)議擴展及其它的默認實現(xiàn)與使用基類,或者其它語言中的抽象類很相似,但是它有幾個關鍵的優(yōu)勢:
- 因為一個類型可以遵守多個協(xié)議,所以它可以從各個協(xié)議中接收到不同的默認實現(xiàn)。與其它語言中所支持的多重繼承不同(吐槽:說的就是C++吧),協(xié)議擴展不會為遵守它的類型增加額外的狀態(tài)。
- 所有的類,結構體和枚舉都可以遵守協(xié)議,而基類只能被類所繼承。
換句說就是,協(xié)議擁有為值類型(value types)增加默認實現(xiàn)的能力,而不僅僅是類。
我們已經(jīng)體驗過了結構體的實戰(zhàn),接下來在 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 能夠遵守這兩個協(xié)議。因為這個枚舉同時遵守了 Bird 和 Flyable 所以它也得到了 canFly 的默認實現(xiàn)。
看到這里,你是不是以為這篇教程只是為了演示一些小鳥的飛行把戲?
接下來,讓我們看一些更有實戰(zhàn)意義的代碼。
擴展協(xié)議(Extending Protocols)
協(xié)議擴展最常用的約莫就是擴展外部協(xié)議了,不管這些協(xié)議來自 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 進行了擴展,定義了一個方法。這個方法會對一個集合類型中的元素以 skip 步進行跳躍,然后返回集合中沒有被跳過的元素。
在 Swift 中,CollectionType 協(xié)議被類似數(shù)組以及字典這樣的集合類型所遵守。這意味著,現(xiàn)在你的整個 app 中,所有遵守 CollectionType 的類型都擁有這個方法了。接著在底部增加下面的代碼:
let bunchaBirds: [Bird] =
[UnladenSwallow.African,
UnladenSwallow.European,
UnladenSwallow.Unknown,
Penguin(name: "King Penguin"),
SwiftBird(version: 2.0),
FlappyBird(name: "Felipe", flappyAmplitude: 3.0, flappyFrequency: 20.0)]
bunchaBirds.skip(3)
這里我們定義了一個數(shù)組,這個數(shù)組包含了大部分之前定義過的鳥類。因為數(shù)組類型遵守了 CollectionType 協(xié)議,所以,我們可以直接對這個數(shù)組使用 skip 方法。
擴展自己的協(xié)議(Extending Your Own Protocols)
跟為標準庫增加方法同樣令人興奮的是,我們也可以為它增加默認行為。
修改鳥類的協(xié)議,使其遵守 BooleanType 協(xié)議:
protocol Bird: BooleanType {
遵守 BooleanType 協(xié)議意味著所有 Bird 類型都要有一個 boolValue 屬性,使得它能夠像布爾值一樣被使用。這是不是說我們得為所有的定義過的,還有將來要定義的 Bird 類型添加這個屬性?
當然不是,使用協(xié)議擴展能讓我們有更簡便的辦法。在 Bird 的定義下面添加如下代碼:
extension BooleanType where Self: Bird {
var boolValue: Bool {
return self.canFly
}
}
這個擴展可以讓 canFly 屬性來表示每個 Bird 類型的布爾值。
通過下面的代碼來試驗一下:
if UnladenSwallow.African {
print("I can fly!")
} else {
print("Guess I’ll just sit here :[")
}
可以看到控制臺打印出了 "I can fly!"。然而更值得注意的是,我們這里直接把 UnladenSwallow.African 丟到了 if 判斷里面!
對 Swift 標準庫的影響
我們已經(jīng)看到了,使用協(xié)議擴展可以極大地增強了我們代碼的功能和可定制性。然而你可能不知道的是,Swift 開發(fā)小組甚至使用了協(xié)議擴展對 Swift 標準庫的寫法進行了改進。
Swift 通過在標準庫中增加了 map、reduce 和 filter 等方法,使它自身的函數(shù)式編程屬性得到了大大的提升。
這些方法存在于不同的 CollectionType 成員中,比如 Array:
// Counts the number of characters in the array
["frog", "pants"].map { $0.length }.reduce(0) { $0 + $1 } // returns 9
對數(shù)組調用 map 方法返回了另一個數(shù)組,再對這個數(shù)組調用 reduce 方法,使其計算出整個數(shù)組中的字符數(shù)。
在這里,map 和 reduce 是包含在 Array當中的。如果我們按住 command 鍵點擊 map,就可以看到它的定義。
在 Swift 1.2 里,我們可以看到類似下面的定義:
// Swift 1.2
extension Array : _ArrayType {
/// Return an `Array` containing the results of calling
/// `transform(x)` on each element `x` of `self`
func map<U>(transform: (T) -> U) -> [U]
}
在這里 map 函數(shù)是作為 Array 的擴展被定義的。但是 Swift 的函數(shù)式函數(shù)不止是對 Array ,而是對所有的 CollectionType 都起作用,那么 Swift 1.2 是如何處理的呢?
如果對一個 Range 類型調用 map 函數(shù),并且從那里跳到它的實現(xiàn),我們可以看到如下的定義:
// Swift 1.2
extension Range {
/// Return an array containing the results of calling
/// `transform(x)` on each element `x` of `self`.
func map<U>(transform: (T) -> U) -> [U]
}
可以發(fā)現(xiàn),對于 Swift 1.2 來說,標準庫中不同的 CollectionType 都需要重新實現(xiàn) map 函數(shù)。
這是因為雖然 Array 與 Range 都遵守了 CollectionType 協(xié)議,但是由于結構體不能被繼承,因此也就無法定義通用的實現(xiàn)。
這不僅僅是標準庫實現(xiàn)上的一點細微差別,這實際上限制對 Swift 類型的使用。
下面這個范型函數(shù)接受一個 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 當中沒有協(xié)議擴展,因此這段代碼會報錯。map 和 reduce 函數(shù)只存在預定義的一些類型中,并不能對任意的 CollectionType 起作用。
然而在 Swift 2.0 中使用了協(xié)議擴展,對于 Array 和 Range 的 map 函數(shù)都是這樣定義的:
// Swift 2.0
extension CollectionType {
/// Return an `Array` containing the results of mapping `transform`
/// over `self`.
///
/// - Complexity: O(N).
func map<T>(@noescape transform: (Self.Generator.Element) -> T) -> [T]
}
雖然無法看到 map 方法的實現(xiàn) -- 至少在 Swift 2.0 開源之前,但是我們可以知道所有的 CollectionType 都有一個 map 方法的默認實現(xiàn)。即是說,所有遵守 CollectionType 的類型,都能隨機附贈一個 map 方法。
在 playground 的最底部增加如下的泛型函數(shù):
func topSpeed<T: CollectionType where T.Generator.Element == Flyable>(c: T) -> Double {
return c.map { $0.airspeedVelocity }.reduce(0) { max($0, $1) }
}
可以對保存 Flyable 類型的集合調用 map 和 reduce 方法了。如果,你還對上面定義的鳥類中哪個速度最快持有疑惑的話,就可以使用這個函數(shù)來得到最終的答案了:
let flyingBirds: [Flyable] =
[UnladenSwallow.African,
UnladenSwallow.European,
SwiftBird(version: 2.0)]
topSpeed(flyingBirds) // 2000.0
接下來做什么(Where To Go From Here)
你可以在這里下載到完整的 `playground。
通過定義自己的簡單協(xié)議,并對它們使用協(xié)議擴展,我們已經(jīng)見識到了面向協(xié)議編程的強大了。通過默認實現(xiàn),我們可以給已經(jīng)存在的協(xié)議增加通用和默認的行為,這與使用基類相似,但是更靈活,因為它也適用于結構體和枚舉。
更進一步,協(xié)議擴展不僅可以用來擴展自定義的協(xié)議,還可以對 Swift 標準庫,Cocoa 和 CocoaTouch 的協(xié)議進行擴展,并提供默認行為。
如果想知道 Swift 2 還更新了哪些其它新特性,可以參考我們的另一篇文章Swift 2 新特性,或者 Swift 2 公布的官方博客。
同時還可以觀看 WWDC 的Protocol Oriented Programming來進行更加深入的學習,以及獲得更底層的理論知識。
最后再打個小廣告:),本人博客地址:Swiftyper