版本記錄
| 版本號 | 時間 |
|---|---|
| V1.0 | 2020.02.14 星期五 |
前言
Swift作為一門開發(fā)語言,它也有自己的特點和對應的編程特點,接下來我們就一起看一下這門語言。讓我們一起熟悉和學習它。感興趣的可以看下面幾篇。
1. Swift編程思想(一) —— 函數(shù)式編程簡介(一)
2. Swift編程思想(二) —— 函數(shù)式編程簡介(二)
開始
首先看下主要內容
在此面向協(xié)議的編程教程中,您將學習
extensions,默認實現(xiàn)和其他將抽象添加到代碼中的技術。
下面看下寫作環(huán)境
Swift 5, iOS 13, Xcode 11
協(xié)議Protocols是Swift的基本功能。 它們在Swift標準庫的結構中起著主導作用,并且是一種常見的抽象方法。 它們?yōu)槟承┢渌Z言提供的接口提供了類似的體驗。
本教程將向您介紹稱為面向協(xié)議的編程(protocol-oriented programming)的軟件工程實踐,這已成為Swift的基礎。 如果您正在學習Swift,這確實是您需要掌握的東西!
在本教程中,您將了解:
- 面向對象的編程和面向協(xié)議的編程之間的區(qū)別。
- 具有默認實現(xiàn)的協(xié)議。
- 擴展Swift標準庫。
- 使用泛型進一步擴展協(xié)議。
你在等什么? 是時候啟動您的Swift引擎了!
假設您正在開發(fā)賽車視頻游戲。您希望玩家能夠駕駛汽車,騎摩托車和駕駛飛機。他們甚至可以騎不同的鳥(因為這是視頻游戲),您可以隨心所欲地駕駛!這里的關鍵是可以驅動或操縱許多不同的“事物”。
此類應用程序的一種常見方法是面向對象的編程,您可以在其中封裝所有邏輯,然后將其繼承給其他類?;愔袑榜{駛”和“飛行員”邏輯。
您可以通過為每種車輛創(chuàng)建類來開始對游戲進行編程。現(xiàn)在在鳥概念中使用大頭針。稍后您將進行處理。
在編寫代碼時,您會注意到Car和Motorcycle共享一些功能,因此您創(chuàng)建了一個稱為MotorVehicle的基類并將其添加到其中。然后,Car和Motorcycle將從MotorVehicle繼承。您還設計了一個名為Aircraft的基類,Plane繼承自該基類。
您認為,“這很好?!笨墒堑鹊龋∧馁愜囉螒蛟O定為30XX年,有些汽車可以飛行。
現(xiàn)在,您面臨困境。 Swift不支持多重繼承。您的飛行汽車如何從MotorVehicle和Aircraft繼承?您是否創(chuàng)建另一個合并了兩個功能的基類?可能不是,因為沒有干凈簡便的方法可以做到這一點。
誰能從這場災難性的困境中拯救您的賽車游戲?面向協(xié)議的編程可以解救!

Why Protocol-Oriented Programming?
協(xié)議允許您將相似的方法,函數(shù)和屬性分組。 Swift可讓您在類,結構和枚舉類型上指定這些接口保證。 只有class類型可以使用基類和繼承。
Swift中協(xié)議的優(yōu)點是對象可以遵循多種協(xié)議。
以這種方式編寫應用程序時,您的代碼將變得更加模塊化。 將協(xié)議視為功能的構建塊。 通過使對象符合協(xié)議來添加新功能時,您無需構建全新的對象。 那很費時間。 而是,添加不同的構造塊,直到對象準備就緒為止。
將基類轉換為協(xié)議可以解決您的視頻游戲難題。 使用協(xié)議,您可以創(chuàng)建同時符合MotorVehicle和Aircraft的FlyingCar類。 整潔吧?
是時候動手實踐一下這個賽車概念了。

Hatching the Egg
首先打開Xcode,然后創(chuàng)建一個名為SwiftProtocols.playground的新playground。 然后添加以下代碼:
protocol Bird {
var name: String { get }
var canFly: Bool { get }
}
protocol Flyable {
var airspeedVelocity: Double { get }
}
使用Command-Shift-Return建立playground,以確保其正確編譯。
這段代碼定義了一個簡單的協(xié)議Bird,帶有屬性name和canFly。 然后,它定義了一個名為Flyable的協(xié)議,該協(xié)議具有airspeedVelocity屬性。
在以前的協(xié)議時代,開發(fā)人員將以Flyable作為基類開始,然后依靠對象繼承來定義Bird和其他任何飛行的事物。
但是在面向協(xié)議的編程中,一切都從協(xié)議開始。 此技術使您可以封裝函數(shù)概念,而無需基類。
如您所見,這使整個系統(tǒng)在定義類型時更加靈活。
Defining Protocol-Conforming Types
首先將以下結構定義添加到playground的底部:
struct FlappyBird: Bird, Flyable {
let name: String
let flappyAmplitude: Double
let flappyFrequency: Double
let canFly = true
var airspeedVelocity: Double {
3 * flappyFrequency * flappyAmplitude
}
}
該代碼定義了一個新的名為FlappyBird的結構,該結構同時符合Bird和Flyable協(xié)議。 它的airspeedVelocity是一個包含flappyFrequency和flappyAmplitude的計算屬性。 由于不穩(wěn)定,它會為canFly返回true。
接下來,將以下兩個結構定義添加到playground的底部
struct Penguin: Bird {
let name: String
let canFly = false
}
struct SwiftBird: Bird, Flyable {
var name: String { "Swift \(version)" }
let canFly = true
let version: Double
private var speedFactor = 1000.0
init(version: Double) {
self.version = version
}
// Swift is FASTER with each version!
var airspeedVelocity: Double {
version * speedFactor
}
}
企鵝Penguin是Bird,但它不會飛。 好東西,您沒有采用繼承方法讓所有鳥類都可以飛行(Flyable)!
使用協(xié)議,您可以定義功能組件并使任何相關對象符合它們。
然后,您聲明SwiftBird,但是在我們的游戲中有不同版本的SwiftBird。 version屬性越高,由計算屬性定義的airspeedVelocity越快。
但是,您會看到有冗余。 每種類型的Bird都必須聲明其是否可以飛行canFly-即使系統(tǒng)中已經存在Flyable的概念。 幾乎就像您需要一種定義協(xié)議方法的默認實現(xiàn)的方法一樣。 嗯,這就是協(xié)議擴展的地方。
Extending Protocols With Default Implementations
協(xié)議擴展允許您定義協(xié)議的默認行為。 要實現(xiàn)第一個,請在Bird協(xié)議定義下面插入以下內容:
extension Bird {
// Flyable birds can fly!
var canFly: Bool { self is Flyable }
}
這段代碼定義了Bird的擴展。 它將canFly的默認行為設置為在類型符合Flyable協(xié)議時返回true。 換句話說,任何Flyable可飛鳥都不再需要顯式聲明它可以canFly。 它會像大多數(shù)鳥類一樣飛翔。
現(xiàn)在從FlappyBird,Penguin和SwiftBird中刪除let canFly =...。 再次構造playground。 您會注意到playground仍然可以成功構建,因為協(xié)議擴展現(xiàn)在可以滿足該要求。
Enums Can Play, Too
Swift中的枚舉Enum類型比C和C ++的枚舉功能強大得多。 它們采用許多傳統(tǒng)上僅支持類或結構類型的功能,這意味著它們可以符合協(xié)議。
在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符合Bird和Flyable這兩個協(xié)議。 因為它是這樣的遵循者,所以它也可以使用canFly的默認實現(xiàn)。
Overriding Default Behavior
您的UnladenSwallow類型通過遵循Bird協(xié)議自動收到canFly的實現(xiàn)。 但是,您希望UnladenSwallow.unknown為canFly返回false。
您可以覆蓋默認實現(xiàn)嗎? 你打賭 回到playground的盡頭并添加一些新代碼:
extension UnladenSwallow {
var canFly: Bool {
self != .unknown
}
}
現(xiàn)在,只有.african和.european才能為canFly返回true。 試試看! 在playground的末尾添加以下代碼:
UnladenSwallow.unknown.canFly // false
UnladenSwallow.african.canFly // true
Penguin(name: "King Penguin").canFly // false
構建playground,您會注意到它顯示了上面評論中給出的值。
這樣,您就可以像在面向對象編程中使用虛擬方法(virtual methods)那樣覆蓋屬性和方法。
Extending Protocols
您還可以使自己的協(xié)議與Swift標準庫中的其他協(xié)議保持一致,并定義默認行為。 將您的Bird協(xié)議聲明替換為以下代碼:
protocol Bird: CustomStringConvertible {
var name: String { get }
var canFly: Bool { get }
}
extension CustomStringConvertible where Self: Bird {
var description: String {
canFly ? "I can fly" : "Guess I'll just sit here :["
}
}
符合CustomStringConvertible意味著您的類型需要具有description屬性,以便在需要時將其自動轉換為String。 您沒有定義此屬性到當前和將來的每種Bird類型,而是定義了協(xié)議擴展,CustomStringConvertible僅將其與Bird類型相關聯(lián)。
在playground底部輸入以下內容進行嘗試:
UnladenSwallow.african
構建playground,您應該會在助手編輯器中看到I can fly的字樣。 恭喜你! 您已經擴展了協(xié)議。
Effects on the Swift Standard Library
協(xié)議擴展無法用外殼抓一磅重的椰子,但是您已經知道,它們可以提供一種自定義和擴展命名類型功能的有效方法。 Swift團隊還采用協(xié)議來改進Swift標準庫。
將此代碼添加到playground的末尾:
let numbers = [10, 20, 30, 40, 50, 60]
let slice = numbers[1...3]
let reversedSlice = slice.reversed()
let answer = reversedSlice.map { $0 * 10 }
print(answer)
您也許能夠猜出答案,但是可能令人驚訝的是所涉及的類型。
例如,slice不是Array <Int>,而是ArraySlice <Int>。 這種特殊的包裝器類型充當原始數(shù)組的視圖,提供了一種快速有效的方法來對較大數(shù)組的各個部分執(zhí)行操作。 同樣,reversedSlice是ReversedCollection <ArraySlice <Int >>,這是另一種包裝器類型,具有對原始數(shù)組的視圖。
幸運的是,開發(fā)Swift標準庫的向導將map函數(shù)定義為Sequence協(xié)議的擴展,所有Collection類型都遵循該協(xié)議。 這使您可以像在ReversedCollection上一樣輕松地在Array上調用map,而不會注意到差異。 您很快就會借用這一重要的設計模式。
Off to the Races
到目前為止,您已經定義了幾種符合Bird的類型。 現(xiàn)在,您將在playground的盡頭添加完全不同的內容:
class Motorcycle {
init(name: String) {
self.name = name
speed = 200.0
}
var name: String
var speed: Double
}
這個類與鳥類或飛行無關。 您只想將摩托車與企鵝競賽。 現(xiàn)在該將這些古怪的賽車手帶入起跑線了。
Bringing It All Together
為了統(tǒng)一這些不同的類型,您需要一個通用的賽車協(xié)議。 得益于一種稱為追溯建模(retroactive modeling)的好主意,您甚至可以在不觸及原始模型定義的情況下進行管理。 只需將以下內容添加到您的playground:
// 1
protocol Racer {
var speed: Double { get } // speed is the only thing racers care about
}
// 2
extension FlappyBird: Racer {
var speed: Double {
airspeedVelocity
}
}
extension SwiftBird: Racer {
var speed: Double {
airspeedVelocity
}
}
extension Penguin: Racer {
var speed: Double {
42 // full waddle speed
}
}
extension UnladenSwallow: Racer {
var speed: Double {
canFly ? airspeedVelocity : 0.0
}
}
extension Motorcycle: Racer {}
// 3
let racers: [Racer] =
[UnladenSwallow.african,
UnladenSwallow.european,
UnladenSwallow.unknown,
Penguin(name: "King Penguin"),
SwiftBird(version: 5.1),
FlappyBird(name: "Felipe", flappyAmplitude: 3.0, flappyFrequency: 20.0),
Motorcycle(name: "Giacomo")]
這是這樣做的:
- 1) 首先,定義協(xié)議
Racer。 該協(xié)議定義了您的游戲中可以競爭的所有內容。 - 2) 然后,使所有內容符合
Racer,以便我們所有現(xiàn)有的類型都可以進行比賽。 某些類型(例如Motorcycle)微不足道。 其他,例如UnladenSwallow,則需要更多邏輯。 無論哪種方式,當您完成后,都會有許多一致的Racer類型。 - 3) 在所有類型都位于開始位置的情況下,您現(xiàn)在創(chuàng)建一個
Array <Racer>,其中包含您所創(chuàng)建的每種類型的實例。
構建playground檢查所有編譯。
Top Speed
是時候編寫一個確定賽車手最高速度的函數(shù)了。 將以下代碼添加到playground的末尾:
func topSpeed(of racers: [Racer]) -> Double {
racers.max(by: { $0.speed < $1.speed })?.speed ?? 0.0
}
topSpeed(of: racers) // 5100
該函數(shù)使用Swift標準庫函數(shù)max來找到速度最高的賽車并返回。 如果用戶為賽車手傳入一個空數(shù)組,則返回0.0。
建造playground,您會發(fā)現(xiàn)您先前創(chuàng)建的賽車手的最大速度確實為5100。
Making It More Generic
假設Racers相當大,并且您只想找到一部分參與者的最高速度。 解決方案是將topSpeed(of :)更改為采用Sequence而不是具體Array的任何東西。
用以下函數(shù)替換現(xiàn)有的topSpeed(of :)實現(xiàn):
// 1
func topSpeed<RacersType: Sequence>(of racers: RacersType) -> Double
/*2*/ where RacersType.Iterator.Element == Racer {
// 3
racers.max(by: { $0.speed < $1.speed })?.speed ?? 0.0
}
這可能看起來有點嚇人,但是它是如何分解的:
- 1)
RacersType是此函數(shù)的通用類型。 它可以是符合Swift標準庫的Sequence協(xié)議的任何類型。 - 2)
where子句指定Sequence的Element類型必須符合Racer協(xié)議才能使用此功能。 - 3) 實際的函數(shù)主體與以前相同。
現(xiàn)在,將以下代碼添加到playground的底部:
topSpeed(of: racers[1...3]) // 42
建立playground,您將看到輸出為42的答案。 該函數(shù)現(xiàn)在適用于任何Sequence類型,包括ArraySlice。
Making It More Swifty
這是一個秘密:您可以做得更好。 在`playground的結尾添加以下內容:
extension Sequence where Iterator.Element == Racer {
func topSpeed() -> Double {
self.max(by: { $0.speed < $1.speed })?.speed ?? 0.0
}
}
racers.topSpeed() // 5100
racers[1...3].topSpeed() // 42
從Swift標準庫劇本中借用,您現(xiàn)在擴展了Sequence本身,使其具有topSpeed()函數(shù)。 該函數(shù)很容易發(fā)現(xiàn),僅在處理Sequence的Racer類型時才適用。
Protocol Comparators
Swift協(xié)議的另一個功能是如何表示運算符要求,例如==的對象相等,或如何比較>和<的對象。 您已知道這筆交易-將以下代碼添加到playground的底部:
protocol Score {
var value: Int { get }
}
struct RacingScore: Score {
let value: Int
}
擁有Score協(xié)議意味著您可以編寫以相同方式對待所有分數(shù)的代碼。 但是,通過使用不同的具體類型(例如RacingScore),您不會將這些分數(shù)與樣式分數(shù)或可愛分數(shù)混為一談。 謝謝,編譯器!
您希望分數(shù)具有可比性,這樣您就可以分辨出誰得分最高。 在Swift 3之前,開發(fā)人員需要添加全局運算符功能以符合這些協(xié)議。 今天,您可以將這些靜態(tài)方法定義為模型的一部分。 為此,將Score和RacingScore的定義替換為以下內容:
protocol Score: Comparable {
var value: Int { get }
}
struct RacingScore: Score {
let value: Int
static func <(lhs: RacingScore, rhs: RacingScore) -> Bool {
lhs.value < rhs.value
}
}
真好! 您已經將RacingScore的所有邏輯封裝在一個地方。 Comparable只需要您為小于運算符提供一個實現(xiàn)。 其余要比較的運算符(例如大于)具有Swift標準庫基于小于運算符提供的默認實現(xiàn)。
在playground底部使用以下代碼行測試新發(fā)現(xiàn)的操作符技能:
RacingScore(value: 150) >= RacingScore(value: 130) // true
建立playground,您會注意到答案是true。 您現(xiàn)在可以比較分數(shù)了!
Mutating Functions
到目前為止,您實現(xiàn)的每個示例都演示了如何添加函數(shù)。 但是,如果您想讓協(xié)議定義一些可以改變對象外觀的東西,該怎么辦? 您可以通過在協(xié)議中使用可變方法來做到這一點。
在playground的底部,添加以下新協(xié)議:
protocol Cheat {
mutating func boost(_ power: Double)
}
這定義了一種協(xié)議,可使您的類型作弊。 怎么樣? 通過增加您認為合適的任何東西。
接下來,使用以下代碼在SwiftBird上創(chuàng)建一個符合Cheat的擴展:
extension SwiftBird: Cheat {
mutating func boost(_ power: Double) {
speedFactor += power
}
}
在這里,您實現(xiàn)boost(_ :)并通過傳入的power使speedFactor增加。您添加了mutating關鍵字,以使該結構體知道其值之一將在此函數(shù)中更改。
將以下代碼添加到playground上,以了解其工作原理:
var swiftBird = SwiftBird(version: 5.0)
swiftBird.boost(3.0)
swiftBird.airspeedVelocity // 5015
swiftBird.boost(3.0)
swiftBird.airspeedVelocity // 5030
在這里,您已經創(chuàng)建了一個可變的SwiftBird,并將其速度提高了三倍,然后又提高了三倍。 構建playground,您應該注意到SwiftBird的airspeedVelocity隨著每次增強而增加。
要繼續(xù)學習有關協(xié)議的更多信息,請閱讀Swift的官方文檔official Swift documentation。
您可以在Apple的開發(fā)人員門戶上觀看有關面向協(xié)議的編程的WWDC精彩會議an excellent WWDC session。 它提供了對所有背后理論的深入探索。
與任何編程范例一樣,很容易變得過于旺盛并將其用于所有事物。 克里斯·艾德霍夫(Chris Eidhof)的這篇有趣的博客文章blog post by Chris Eidhof提醒讀者,他們應該提防銀子彈解決方案。 不要在各處僅因為協(xié)議“而使用”。
后記
本篇主要講述了基于Swift5.1的面向協(xié)議編程,感興趣的給個贊或者關注~~~
