協(xié)議(Protocol)是 Swift 的基礎(chǔ)功能。在 Swift 的標(biāo)準(zhǔn)庫(kù)中起著主導(dǎo)作用掀泳,并且是一種常見的抽象方法况既。Protocol 提供了與其他語(yǔ)言類似的接口功能。
這篇文章將介紹面向協(xié)議編程(Protocol Oriented Programming属瓣,簡(jiǎn)稱 POP)载迄,面向協(xié)議編程是 Apple 在 WWDC2015 上提出的一種編程范式讯柔,其已成為 Swift 的基礎(chǔ)。與傳統(tǒng)的面向?qū)ο缶幊蹋∣bject Oriented Programming护昧,簡(jiǎn)稱 OOP)相比魂迄,POP 更為靈活。如果你正在學(xué)習(xí) Swift惋耙,應(yīng)掌握面向協(xié)議編程捣炬。
本文將涉及以下幾個(gè)方面:
- 面向?qū)ο缶幊膛c面向協(xié)議編程的區(qū)別。
- 協(xié)議的默認(rèn)實(shí)現(xiàn)绽榛。
- 擴(kuò)展 Swift 標(biāo)準(zhǔn)庫(kù)湿酸。
- 協(xié)議支持范型。
1. 介紹
假設(shè)你在開發(fā)一款賽車游戲灭美,希望玩家能夠駕駛汽車推溃、摩托車和飛機(jī),甚至可以騎不同的鳥飛行届腐。這里的關(guān)鍵是可以操作不同的設(shè)備铁坎。
一種常見的方案是使用面向?qū)ο缶幊蹋瑢⑺羞壿嫹庋b到基類梯捕,其他類繼承自基類厢呵。因此,基類需要有駕駛傀顾、飛行等各種邏輯襟铭。
開發(fā)過程中為每個(gè)設(shè)備創(chuàng)建一個(gè)類。編程過程中短曾,你會(huì)發(fā)現(xiàn)Car
寒砖、Motorcycle
有一些共用功能,你可能需要?jiǎng)?chuàng)建一個(gè)共同的父類MotorVehicle
實(shí)現(xiàn)共用功能嫉拐。此外哩都,還會(huì)創(chuàng)建一個(gè)Aircraft
基類實(shí)現(xiàn)飛行相關(guān)功能,Plane
繼承自Aircraft
婉徘。
隨著需求的迭代漠嵌,后續(xù)可能需要增加會(huì)飛的汽車。Swift 不支持多重繼承盖呼,應(yīng)如何同時(shí)繼承自MotorVehicle
和Aircraft
儒鹿?是否需要?jiǎng)?chuàng)建另一個(gè)基類,實(shí)現(xiàn)MotorVehicle
几晤、Aircraft
的功能约炎?當(dāng)然,也可以通過 Runtime 的消息轉(zhuǎn)發(fā)實(shí)現(xiàn)多重繼承,但其不利于維護(hù)圾浅,也不優(yōu)雅掠手。
面向協(xié)議編程可以很好解決這一問題。
2. 面向協(xié)議編程
協(xié)議(protocol)允許將相似的方法狸捕、函數(shù)喷鸽、屬性放到一組。Swift 中的class
府寒、enum
和struct
都可以遵守協(xié)議魁衙,但只有class
支持繼承报腔。
與繼承相比株搔,協(xié)議的優(yōu)勢(shì)在于對(duì)象可以遵守多個(gè)協(xié)議。
使用面向協(xié)議編程纯蛾,代碼可以更具模塊化纤房。可以將協(xié)議視為功能塊翻诉,當(dāng)通過遵守新的協(xié)議添加新功能時(shí)炮姨,無(wú)需創(chuàng)建全新的對(duì)象。創(chuàng)建全新的對(duì)象太耗費(fèi)時(shí)間碰煌。相反舒岸,只需增加不同的功能塊。
將基類模式轉(zhuǎn)變面向協(xié)議編程模式芦圾,可以很好解決前面遇到的問題蛾派。使用協(xié)議時(shí),可以創(chuàng)建一個(gè)FlyingCar
類个少,同時(shí)遵守MotorVehicle
和Aircraft
協(xié)議洪乍。
3. 創(chuàng)建協(xié)議
創(chuàng)建一個(gè)名稱為ProtocolOrientedProgramming
的playground,并添加以下代碼:
protocol Bird {
var name: String { get }
var canFly: Bool { get }
}
protocol Flyable {
var airspeedVelocity: Double { get }
}
Bird
協(xié)議有兩個(gè)只讀的屬性夜焦。Flyable
協(xié)議有一個(gè)只讀的屬性壳澳。
在沒有使用面向協(xié)議編程時(shí),開發(fā)者一般創(chuàng)建一個(gè)Flyable
的基類茫经,繼承后實(shí)現(xiàn)子類巷波。使用面向協(xié)議編程后,所有的都以 protocol 開始卸伞,將所有功能封裝到 protocol抹镊,無(wú)需使用繼承。這樣在定義類型時(shí)可以更為靈活瞪慧。
4. 遵守協(xié)議
添加以下struct
:
struct FlappyBird: Bird, Flyable {
var name: String
let flappyAmplitude: Double
let flappyFrequency: Double
let canFly = true
var airspeedVelocity: Double {
3 * flappyFrequency * flappyAmplitude
}
}
FlappyBird
結(jié)構(gòu)體遵守了Bird
髓考、Flyable
協(xié)議。airspeedVelocity
是一個(gè)計(jì)算屬性弃酌,FlappyBird
是一種會(huì)飛的鳥氨菇,canFly
返回true
儡炼。
繼續(xù)添加以下結(jié)構(gòu)體:
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
}
var airspeedVelocity: Double {
version * speedFactor
}
}
Penguin
是一種不會(huì)飛的鳥。如果使用了繼承模式查蓉,則會(huì)讓所有鳥會(huì)飛乌询。使用協(xié)議可以定義一組功能類似的組件,任何相關(guān)的對(duì)象都可以遵守該協(xié)議豌研。
SwiftBird
結(jié)構(gòu)體有不同版本妹田,版本越高airspeedVelocity
越快。
每個(gè)遵守Bird
協(xié)議的struct
鹃共、class
都需要實(shí)現(xiàn)canFly
鬼佣,即使已經(jīng)存在了Flyable
協(xié)議。如果能為 protocol 提供默認(rèn)實(shí)現(xiàn)霜浴,重復(fù)代碼將變少晶衷,這也就是 protocol extension 的用途。
5. Protocol Extension
Protocol extension 提供了協(xié)議的默認(rèn)實(shí)現(xiàn)阴孟。以下代碼為Bird
的canFly
提供了默認(rèn)實(shí)現(xiàn):
extension Bird {
// Flyable birds can fly.
var canFly: Bool { self is Flyable }
}
遵守Flyable
協(xié)議的類型canFly
返回true
晌纫,即遵守Bird
協(xié)議的類型無(wú)需重復(fù)實(shí)現(xiàn)canFly
屬性。現(xiàn)在可以刪除FlappyBird
永丝、Penguin
和SwiftBird
中的canFly
屬性锹漱。
6. enum 也可以遵守協(xié)議
Swift 中的enum
比 C、C++ 中的更為強(qiáng)大慕嚷,它支持了以往只能夠用在類哥牍、結(jié)構(gòu)體上的功能。例如闯冷,enum
可以遵守協(xié)議砂心。
添加以下enum
:
// enum也可以遵守協(xié)議
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
使用了 protocol extension 的默認(rèn)實(shí)現(xiàn)蛇耀。
7. 重寫 protocol extension 的默認(rèn)實(shí)現(xiàn)
UnladenSwallow
類型自動(dòng)使用了Bird
協(xié)議canFly
屬性的默認(rèn)實(shí)現(xiàn)辩诞,使用以下代碼可以重寫默認(rèn)實(shí)現(xiàn):
extension UnladenSwallow {
var canFly: Bool {
self != .unknown
}
}
只有在.african
和.european
時(shí)canFly
返回true
。使用以下代碼進(jìn)行驗(yàn)證:
UnladenSwallow.unknown.canFly // false
UnladenSwallow.african.canFly // true
Penguin(name: "King Penguin").canFly // false
使用上述方法纺涤,可以像面向?qū)ο缶幊桃粯又貙憣傩砸朐荨⒎椒ā?/p>
8. 擴(kuò)展協(xié)議
還可以讓自己創(chuàng)建的協(xié)議遵守 Swift 標(biāo)準(zhǔn)庫(kù)中協(xié)議,同時(shí)定義其默認(rèn)實(shí)現(xiàn)撩炊。更新Bird
協(xié)議如下:
// Bird協(xié)議遵守CustomStringConvertible協(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"
}
}
Bird
協(xié)議遵守了CustomStringConvertible
協(xié)議,CustomStringConvertible
協(xié)議只有一個(gè)實(shí)例屬性description
拧咳,實(shí)現(xiàn)后可以提供自定義輸出伯顶。CustomStringConvertible
只為Bird
類型提供了 protocol extension。
添加以下代碼:
UnladenSwallow.african
使用Shift + Command + Enter快捷鍵運(yùn)行 playground,可以看到 assistant editor 區(qū)域輸出的I can fly
祭衩。
9. 使用 protocol extension 擴(kuò)展 Swift 標(biāo)準(zhǔn)庫(kù)
Protocol extension 提供了一種擴(kuò)展命名類的功能灶体,Swift 團(tuán)隊(duì)也使用 protocol 改進(jìn) Swift 標(biāo)準(zhǔn)庫(kù)。
添加以下代碼:
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
是ArraySlice<Int>
類型掐暮,而非Array<Int>
類型蝎抽。該包裝類型提供了一種快速、高效的方式操作數(shù)組的一部分路克。reversedSlice
是ReversedCollection<ArraySlice<Int>>
類型樟结,也是對(duì)數(shù)組的一種包裝。
map
函數(shù)是在Sequence
協(xié)議extension中實(shí)現(xiàn)的精算,所有Collection
類型都遵守了Sequence
協(xié)議瓢宦。因此,可以在Array
殖妇、ReversedCollection
中使用map
函數(shù)刁笙,且使用過程中沒有區(qū)別破花。
10. 查找最高分
目前谦趣,已經(jīng)有多種類型遵守Bird
協(xié)議。下面添加以下代碼到 playground:
class Motorcycle {
init(name: String) {
self.name = name
speed = 200.0
}
var name: String
var speed: Double
}
Motorcycle
類與Bird
座每、Flying
協(xié)議無(wú)關(guān)前鹅,其也可以與其他類型競(jìng)賽。
為了統(tǒng)一不同類型峭梳,需要一個(gè)單獨(dú)競(jìng)賽 protocol舰绘,如下所示:
// 聲明Racer協(xié)議,指定競(jìng)賽的指標(biāo)葱椭。
protocol Racer {
var speed: Double { get }
}
// 下面類型均遵守了Racer協(xié)議捂寿,即均可以進(jìn)行比賽。
extension FlappyBird: Racer {
var speed: Double {
airspeedVelocity
}
}
extension SwiftBird: Racer {
var speed: Double {
airspeedVelocity
}
}
extension Penguin: Racer {
var speed: Double {
42
}
}
extension UnladenSwallow: Racer {
var speed: Double {
canFly ? airspeedVelocity : 0.0
}
}
extension Motorcycle: Racer { }
// 數(shù)組中實(shí)例均遵守了Racer協(xié)議
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")
]
10.1 單獨(dú)方法查找
使用以下函數(shù)查找速度最快的競(jìng)賽者:
/// 查找速度最快的選手
func topSpeed(of racers: [Racer]) -> Double {
racers.max(by: { $0.speed < $1.speed })?.speed ?? 0.0
}
topSpeed(of: racers)
topSpeed(of:)
函數(shù)返回最快選手的速度孵运。如果傳入數(shù)組為空秦陋,則返回0.0。執(zhí)行后其速度是5100治笨。
10.2 范型查找
假設(shè)Racers
數(shù)量眾多驳概,目前只需查找部分參與者的最快速度。那么應(yīng)修改topSpeed(of:)
函數(shù)參數(shù)為Sequence
類型旷赖,而非數(shù)組顺又。如下所示:
// RacersType是范型,需遵守Sequence協(xié)議等孵。
// where語(yǔ)句指定Sequence的元素必須遵守Racer協(xié)議稚照。
func topSpeed<RacersType: Sequence>(of racers: RacersType) -> Double where RacersType.Iterator.Element == Racer {
racers.max(by: { $0.speed < $1.speed })?.speed ?? 0.0
}
使用以下代碼查看指定范圍數(shù)組元素速度:
topSpeed(of: racers[1...3])
運(yùn)行后輸出42。該函數(shù)目前支持所有Sequence
類型,包括ArraySlice
果录。
10.3 為 Sequence 增加 extension
還可以進(jìn)一步優(yōu)化查找topSpeed選手的方法腌闯,優(yōu)化后如下:
// 當(dāng)Sequence的元素為Racer類型時(shí),為其添加topSpeed方法雕憔。
extension Sequence where Iterator.Element == Racer {
func topSpeed() -> Double {
self.max(by: { $0.speed < $1.speed })?.speed ?? 0.0
}
}
racers.topSpeed()
racers[1...3].topSpeed()
參照 Swift 標(biāo)準(zhǔn)庫(kù)的實(shí)現(xiàn)姿骏,擴(kuò)展了Sequence
協(xié)議,增加了topSpeed()
方法斤彼,且只有在Sequence
元素是Racer
類型時(shí)可用分瘦。
11. 使用協(xié)議比較大小
Swift 協(xié)議還可以用來比較大小。例如琉苇,比較對(duì)象是否相等==嘲玫、大于>和小于<。
添加以下代碼:
protocol Score {
var value: Int { get }
}
struct RacingScore: Score {
let value: Int
}
有了Score
協(xié)議并扇,后續(xù)所有處理都可以根據(jù)Score
來進(jìn)行去团,無(wú)需關(guān)注具體類型。
讓score可比較就可以很方便的查找到最高分?jǐn)?shù)穷蛹,更新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
}
}
Comparable
協(xié)議需要提供小于操作的實(shí)現(xiàn)土陪。Swift標(biāo)準(zhǔn)庫(kù)會(huì)根據(jù)提供的小于操作,自動(dòng)實(shí)現(xiàn)其他類型的比較操作肴熏。
RacingScore(value: 150) >= RacingScore(value: 130) // true
運(yùn)行后鬼雀,上述代碼打印true
。
12. mutating
截至目前蛙吏,所有演示都是在增加功能源哩。如何使用 protocol 改變對(duì)象的屬性呢?可以使用mutating
方法實(shí)現(xiàn)鸦做,如下所示:
protocol Cheat {
mutating func boost(_ power: Double)
}
Cheat
協(xié)議內(nèi)函數(shù)可以修改對(duì)象內(nèi)屬性励烦。讓SwiftBird
遵守Cheat
協(xié)議,如下所示:
extension SwiftBird: Cheat {
// 修改speedFactor泼诱,讓其增加power坛掠。
mutating func boost(_ power: Double) {
speedFactor += power
}
}
修改struct結(jié)構(gòu)體內(nèi)元素時(shí),函數(shù)需使用mutating
標(biāo)記坷檩。
使用以下代碼查看boost(_:)
如何工作:
// 創(chuàng)建可變對(duì)象
var swiftBird = SwiftBird(version: 5.0)
// 速度增加3
swiftBird.boost(3.0)
swiftBird.airspeedVelocity // 5015
// 速度再次增加3
swiftBird.boost(3.0)
swiftBird.airspeedVelocity // 5030
運(yùn)行后却音,可以看到SwiftBird
的airspeedVelocity
速度增加了。
總結(jié)
現(xiàn)在已經(jīng)介紹了面向協(xié)議編程的優(yōu)勢(shì)矢炼。通過默認(rèn)實(shí)現(xiàn)系瓢,可以為已經(jīng)存在的協(xié)議提供基礎(chǔ)功能。這一點(diǎn)類似于繼承中的基類句灌,但可用于struct
夷陋、enum
欠拾。
Demo名稱:ProtocolOrientedProgramming
源碼地址:https://github.com/pro648/BasicDemos-iOS/tree/master/ProtocolOrientedProgramming
參考資料:
- 面向協(xié)議編程與 Cocoa 的邂逅 (上)
- Protocol-Oriented Programming Tutorial in Swift 5.1: Getting Started
- Protocol-Oriented Programming in Swift WWDC2015
- Protocol Oriented Programming is Not a Silver Bullet
歡迎更多指正:https://github.com/pro648/tips
本文地址:https://github.com/pro648/tips/blob/master/sources/面向協(xié)議編程.md