最近有時(shí)間萧恕,挑了幾個(gè)今年WWDC中比較感興趣的Session視頻來(lái)學(xué)習(xí)姑子,今天就抽時(shí)間整理一下關(guān)于Swift 2.0
中一個(gè)比較新的概念面向協(xié)議編程
乎婿。
相關(guān)的Session視頻鏈接如下:
- Session 408: Protocol-Oriented Programming in Swift
- Session 414: Building Better Apps with Value Types in Swift
寫在前面
面向協(xié)議編程是什么?
你可能聽過(guò)類似的概念:面向?qū)ο缶幊?/strong>街佑、函數(shù)式編程谢翎、泛型編程,再加上蘋果今年新提出的面向協(xié)議編程舆乔,這些統(tǒng)統(tǒng)可以理解為是一種編程范式岳服。所謂編程范式,是隱藏在編程語(yǔ)言背后的思想希俩,代表著語(yǔ)言的作者想要用怎樣的方式去解決怎樣的問(wèn)題吊宋。不同的編程范式反應(yīng)在現(xiàn)實(shí)世界中,就是不同的編程語(yǔ)言適用于不同的領(lǐng)域和環(huán)境,比如在面向?qū)ο缶幊趟枷胫辛眩_發(fā)者用對(duì)象來(lái)描述萬(wàn)事萬(wàn)物并試圖用對(duì)象來(lái)解決所有可能的問(wèn)題拖吼。編程范式都有其各自的偏好和使用限制,所以越來(lái)越多的現(xiàn)代編程語(yǔ)言開始支持多范式这吻,使語(yǔ)言自身更強(qiáng)壯也更具適用性吊档。
更多編程范式和相關(guān)概念請(qǐng)參看:維基百科:編程范式
對(duì)Swift語(yǔ)言所采用的編程范式感興趣的朋友可以參看這篇文章:多范式編程語(yǔ)言-以 Swift 為例
面向協(xié)議編程長(zhǎng)什么樣子?
在詳細(xì)解釋面向協(xié)議編程之前唾糯,我們先簡(jiǎn)單地概括一下面向協(xié)議編程長(zhǎng)什么樣子怠硼?它與我們熟悉的面向?qū)ο缶幊逃惺裁床灰粯樱?/p>
簡(jiǎn)單來(lái)說(shuō),面向協(xié)議編程是在面向?qū)ο缶幊袒A(chǔ)上演變而來(lái)移怯,將程序設(shè)計(jì)過(guò)程中遇到的數(shù)據(jù)類型的抽认懔А(抽象)由使用基類進(jìn)行抽取改為使用協(xié)議(Java語(yǔ)言中的接口)進(jìn)行抽取。更簡(jiǎn)單點(diǎn)舉個(gè)栗子來(lái)說(shuō)舟误,一個(gè)貓類葡秒、一個(gè)狗類,我們很容易想到抽取一個(gè)描述動(dòng)物的基類嵌溢,也會(huì)有人想到抽取一個(gè)動(dòng)物通用的協(xié)議眯牧,那后者就可以被叫做面向協(xié)議編程了。什么赖草?就是這樣而已学少?蘋果官方那么正式的稱Swift是一門支持面向協(xié)議編程的語(yǔ)言,難道就是這么簡(jiǎn)單的內(nèi)容疚顷?當(dāng)然不會(huì)旱易,有過(guò)面向?qū)ο缶幊探?jīng)驗(yàn)的人都會(huì)清楚,協(xié)議的使用限制很多腿堤,并不能適用于大多數(shù)情況下數(shù)據(jù)類型的抽象。而在Swift語(yǔ)言中如暖,協(xié)議被賦予了更多的功能和更廣闊的使用空間笆檀,在Swift 2.0中,更為協(xié)議增加了擴(kuò)展功能盒至,使其能夠勝任絕大多數(shù)情況下數(shù)據(jù)類型的抽象酗洒,所以蘋果開始聲稱Swift是一門支持面向協(xié)議編程的語(yǔ)言。
面向協(xié)議編程對(duì)比面向?qū)ο缶幊痰暮锰幵谀睦锛纤欤克鼤?huì)對(duì)我們程序的設(shè)計(jì)造成哪些影響樱衷?我們會(huì)在下文中繼續(xù)分析。
寫在中間
離開面向?qū)ο笪覀兪チ耸裁矗?/h4>
首先酒唉,讓我們來(lái)看看面向?qū)ο缶幊虨槲覀儙?lái)的好處矩桂。絕大多數(shù)熟悉一種或幾種面向?qū)ο缶幊陶Z(yǔ)言的開發(fā)者都能隨口說(shuō)出幾條面向?qū)ο缶幊痰膬?yōu)點(diǎn),比如數(shù)據(jù)的封裝痪伦、數(shù)據(jù)訪問(wèn)的控制侄榴、數(shù)據(jù)類型的抽象雹锣、代碼的可讀性和可擴(kuò)展性等。這意味著離開了面向?qū)ο缶幊涛覀円簿褪チ巳绱硕嗟暮锰帯?/p>
哦癞蚕,天吶蕊爵!不要這樣好嘛?
回頭仔細(xì)想想桦山,這些好處只有面向?qū)ο缶幊滩庞新镌苌洌刻O果給了我們另一種答案:It's Type, not Classes,是抽象的類型帶給我們?nèi)绱硕嗟暮锰幒闼⒉皇敲嫦驅(qū)ο笾械?strong>類会放,類只是抽象類型的一種方式。比如在Swift語(yǔ)言中寇窑,使用結(jié)構(gòu)體和枚舉也同樣能夠?qū)崿F(xiàn)對(duì)類型的抽象鸦概、數(shù)據(jù)的封裝和訪問(wèn)控制等,這些好處又都回來(lái)了甩骏。
那么有沒(méi)有什么是類能帶給我們窗市,而結(jié)構(gòu)體和枚舉辦不到的呢?當(dāng)然有饮笛,不然我們真的可以離開面向?qū)ο罅俗刹臁C嫦驅(qū)ο缶幊踢€有兩個(gè)非常重要的特性我們還沒(méi)有提到:繼承和多態(tài)。繼承和多態(tài)為我們帶來(lái)了豐富多彩的世界福青,想想我們Cocoa Touch中的框架摄狱,這才是我們所熟悉的面向?qū)ο缶幊蹋刮覀兡軌蜉p易地解決所面對(duì)的問(wèn)題无午,并使我們的代碼具有高度的可定制和可重用性媒役。
我們的世界終于好像正常了。
擁有面向?qū)ο笪覀冇值玫搅耸裁矗?/h4>
那么宪迟,面向?qū)ο缶幊淘趲Ыo我們這么多好處的同時(shí)酣衷,是否還附帶了其他一些特性呢?比如說(shuō):要花費(fèi)的代價(jià)次泽。
我們先來(lái)看出現(xiàn)的第一個(gè)問(wèn)題穿仪,多數(shù)面向?qū)ο笳Z(yǔ)言中的對(duì)象都是使用引用類型,在對(duì)象傳遞過(guò)程中只是將引用復(fù)制一份并指向原有的對(duì)象意荤,這樣就會(huì)出現(xiàn)問(wèn)題啊片。比如下面代碼所示的例子:
class Book {
var name: String
var pages: Int
init(name: String, pages: Int) {
self.name = name
self.pages = pages
}
}
class Person {
var name: String
var book: Book
init(name: String, book: Book) {
self.name = name
self.book = book
}
}
let 圍城 = Book(name: "圍城", pages: 888)
let 小明 = Person(name: "小明", book: 圍城) // 小明有一本全新的《圍城》
let 小剛 = Person(name: "小剛", book: 圍城) // 小剛也有一本全新的《圍城》
小明.book.pages = 88 // 小明淘氣把書弄壞了,只剩88頁(yè)了
print(小剛.book.pages) // 輸出結(jié)果:88 WTF! Where is my new book?
故事的結(jié)尾是:小剛因?yàn)榕獕臅粙寢尨蛄藒 不對(duì)啊玖像,小明哪去了紫谷?我也不知道~
相信大多數(shù)面向?qū)ο缶幊陶Z(yǔ)言的開發(fā)者都明白這是引用傳遞的原因,通常我們的解決辦法也很簡(jiǎn)單,每次賦值的時(shí)候都先拷貝一份再進(jìn)行賦值。當(dāng)我們嘗試在上述代碼中加入copy方法時(shí),卻發(fā)現(xiàn)在Swift中對(duì)象默認(rèn)并沒(méi)有copy方法痪欲,這是因?yàn)镾wift更推薦使用值類型變量而不是引用類型的變量。如果真的需要調(diào)用copy方法羹膳,你可以將Book類繼承自NSObject,但這樣的做法真的一點(diǎn)都不優(yōu)雅根竿,也不夠Swiftpyer
陵像。實(shí)際上我們的問(wèn)題也可以采用如下的解決辦法:
class Book {
var name: String
var pages: Int
init(name: String, pages: Int) {
self.name = name
self.pages = pages
}
}
class Person {
var name: String
var book: Book
init(name: String, book: Book) {
self.name = name
self.book = Book(name: book.name, pages: book.pages)
}
}
let 圍城 = Book(name: "圍城", pages: 888)
let 小明 = Person(name: "小明", book: 圍城) // 小明有一本全新的《圍城》
let 小剛 = Person(name: "小剛", book: 圍城) // 小剛也有一本全新的《圍城》
小明.book.pages = 88 // 小明淘氣把書弄壞了,只剩88頁(yè)了
print(小剛.book.pages) // 輸出結(jié)果:888
我們?cè)赑erson的構(gòu)造方法中寇壳,為book屬性新創(chuàng)建了一本書醒颖,從而保證小明和小剛各自擁有自己的書。這個(gè)解決辦法可能并不適用于所有引用類型傳遞的情況壳炎,那么在Swift中泞歉,最好的解決辦法是什么呢?其實(shí)答案很簡(jiǎn)單匿辩,使用值類型而非引用類型腰耙。Swift中許多常見的數(shù)據(jù)類型、字符串铲球、集合類型挺庞,以及結(jié)構(gòu)體和枚舉都是值類型而非引用類型,值類型的變量在賦值時(shí)會(huì)自動(dòng)進(jìn)行一次低消耗的值拷貝稼病,對(duì)比對(duì)象的copy要更加高效而且不存在線程安全問(wèn)題选侨。所以我們上面這個(gè)故事的最好結(jié)局是:將Book修改為結(jié)構(gòu)體類型。
struct Book {
var name: String
var pages: Int
init(name: String, pages: Int) {
self.name = name
self.pages = pages
}
}
struct Person {
var name: String
var book: Book
init(name: String, book: Book) {
self.name = name
self.book = book
}
}
let 圍城 = Book(name: "圍城", pages: 888)
var 小明 = Person(name: "小明", book: 圍城) // 小明有一本全新的《圍城》
let 小剛 = Person(name: "小剛", book: 圍城) // 小剛也有一本全新的《圍城》
小明.book.pages = 88 // 小明淘氣把書弄壞了然走,只剩88頁(yè)了
print(小剛.book.pages) // 輸出結(jié)果:888
小剛終于得救了~
想了解更多值類型的使用及其相關(guān)信息可以參看:Session 414: Building Better Apps with Value Types in Swift
我們剛剛使用一個(gè)例子解釋了面向?qū)ο缶幊讨惺褂靡妙愋涂赡艹霈F(xiàn)的問(wèn)題援制,接下來(lái)我們談?wù)摿硪粋€(gè)非常重要的話題:繼承的代價(jià)。這并不是一個(gè)新穎的話題芍瑞,自面向?qū)ο缶幊陶Q生之日起就飽受爭(zhēng)議隘谣,我們經(jīng)常要忍受著愈加繁雜和龐大的繼承體系來(lái)獲得代碼的可重用性,而且隨著繼承層次的增加啄巧,代碼的復(fù)雜性會(huì)加速增長(zhǎng),隨之而來(lái)的bug也會(huì)越來(lái)越難以發(fā)現(xiàn)掌栅。這時(shí)我們可能需要依靠設(shè)計(jì)模式來(lái)找回我們的思路秩仆,然而大多數(shù)設(shè)計(jì)模式只能幫助你理順你的代碼結(jié)構(gòu),卻在同時(shí)更加加深了你的代碼的復(fù)雜度猾封。
繼承帶給我們的另一個(gè)好處就是多態(tài)澄耍,多態(tài)極大地增強(qiáng)了我們代碼的可擴(kuò)展性。然而就像“能量守恒定律”一樣,多態(tài)也帶來(lái)了一定的負(fù)面影響齐莲,那就是類型信息的缺失痢站。形象一點(diǎn)講,就是我們常常會(huì)寫出這樣的代碼:subClassObject as! SubClass
选酗,向下類型轉(zhuǎn)換阵难。
那么問(wèn)題來(lái)了:什么是更好的抽象類型?
蘋果官方對(duì)這個(gè)問(wèn)題的回答如下:
- 更多地支持值類型芒填,同時(shí)也支持引用類型
- 更多地支持靜態(tài)類型關(guān)聯(lián)(編譯期)呜叫,同時(shí)也支持動(dòng)態(tài)派發(fā)(運(yùn)行時(shí))
- 結(jié)構(gòu)不龐大不復(fù)雜
- 模型可擴(kuò)展
- 不給模型強(qiáng)制添加數(shù)據(jù)
- 不給模型增加初始化任務(wù)的負(fù)擔(dān)
- 清楚哪些方法該實(shí)現(xiàn)哪些方法不需實(shí)現(xiàn)
其實(shí)答案就是Swift中的面向協(xié)議編程,蘋果只是在自賣自夸而已殿衰。
面向協(xié)議編程
接下來(lái)我們就正式進(jìn)入Swift的面向協(xié)議編程的世界朱庆。首先我們來(lái)對(duì)比如下兩段示例代碼,代碼的功能是定義一個(gè)更具擴(kuò)展性的二分查找法闷祥。
class Ordered {
func precedes(other: Ordered) -> Bool { fatalError("implement me!") }
}
class Number: Ordered {
var value: Double = 0
override func precedes(other: Ordered) -> Bool {
return self.value < (other as! Number).value
}
}
func binarySearch(sortedKeys: [Ordered], forKey k: Ordered) -> Int {
var lo = 0
var hi = sortedKeys.count
while hi > lo {
let mid = lo + (hi - lo) / 2
if sortedKeys[mid].precedes(k) { lo = mid + 1 }
else { hi = mid }
}
return lo
}
protocol Ordered {
func precedes(other: Self) -> Bool
}
struct Number: Ordered {
var value: Double = 0
func precedes(other: Number) -> Bool {
return self.value < other.value
}
}
func binarySearch<T: Ordered>(sortedKeys: [T], forKey k: T) -> Int {
var lo = 0
var hi = sortedKeys.count
while hi > lo {
let mid = lo + (hi - lo) / 2
if sortedKeys[mid].precedes(k) { lo = mid + 1 }
else { hi = mid }
}
return lo
}
應(yīng)該不難看出兩者之間的區(qū)別以及孰優(yōu)孰劣娱颊,簡(jiǎn)單解釋一下前者的缺點(diǎn),反過(guò)來(lái)也就是后者的優(yōu)點(diǎn)了凯砍。
- OC語(yǔ)言中沒(méi)有抽象類這個(gè)概念箱硕,所有抽象類都是靠文檔注釋標(biāo)明,這很蛋疼~
- 其他類型若想使用該二分查找法果覆,必須繼承自O(shè)rdered抽象類颅痊,在單繼承體系中,該類型將無(wú)法再繼承其他類型
- 方法參數(shù)接收的數(shù)組中局待,類型要求不嚴(yán)格斑响,可以放入多種不同類型的Ordered子類對(duì)象
- 基于前一點(diǎn)原因,為保證嚴(yán)謹(jǐn)性钳榨,必須在方法實(shí)現(xiàn)內(nèi)部增加類型判斷舰罚,這更加蛋疼~~
基于上面的例子,我們可以稍微感受到面向協(xié)議編程在擴(kuò)展性上的優(yōu)勢(shì)了薛耻,這里再提幾個(gè)注意點(diǎn)营罢。
- Swift 2.0新特性之一,將Self用于約束泛型饼齿,功能類似于OC中的instancetype饲漾,示例:
extension Ordered where Self: Comparable
- Swift 2.0另一個(gè)重要的新特性,協(xié)議可擴(kuò)展缕溉,意味著你不僅可以擴(kuò)展一個(gè)類型使其遵守Ordered協(xié)議考传,還可以直接擴(kuò)展某個(gè)協(xié)議,詳見如下兩段代碼示例证鸥。
// 擴(kuò)展類型
extension Int: Ordered {
func precedes(other: Int) -> Bool {
return self < other
}
}
extension String: Ordered {
func precedes(other: String) -> Bool {
return self < other
}
}
let intIndex = binarySearch([2, 3, 5, 7], forKey: 5) // 輸出結(jié)果2
let stringIndex = binarySearch(["2", "3", "5", "7"], forKey: "5") // 輸出結(jié)果2
// 擴(kuò)展協(xié)議:方式一
//extension Comparable {
// func precedes(other: Self) -> Bool {
// return self < other
// }
//}
// 擴(kuò)展協(xié)議:方式二(Swift 2.0的推薦方式)
extension Ordered where Self: Comparable {
func precedes(other: Self) -> Bool {
return self < other
}
}
extension Int: Ordered {}
extension String: Ordered {}
let intIndex = binarySearch([2, 3, 5, 7], forKey: 5) // 輸出結(jié)果2
let stringIndex = binarySearch(["2", "3", "5", "7"], forKey: "5") // 輸出結(jié)果2
從上面的代碼我們可以看出僚楞,協(xié)議可擴(kuò)展所帶來(lái)的功能之一就是能夠?yàn)閰f(xié)議中的方法提供默認(rèn)實(shí)現(xiàn)勤晚。
更多協(xié)議可擴(kuò)展所帶來(lái)的功能可以參看RayWenderlich上的這篇文章:
關(guān)于面向協(xié)議編程的完整示例程序可以參看蘋果官方的示例代碼:
寫在最后
個(gè)人總結(jié)
面向?qū)ο缶幊毯兔嫦騾f(xié)議編程最明顯的區(qū)別在于程序設(shè)計(jì)過(guò)程中對(duì)數(shù)據(jù)類型的抽取(抽象)上泉褐,面向?qū)ο缶幊淌褂妙惡屠^承的手段赐写,數(shù)據(jù)類型是引用類型;而面向協(xié)議編程使用的是遵守協(xié)議的手段膜赃,數(shù)據(jù)類型是值類型(Swift中的結(jié)構(gòu)體或枚舉)挺邀。
面向協(xié)議編程是在面向?qū)ο缶幊袒A(chǔ)上發(fā)展而來(lái)的,而并不是完全背離面向?qū)ο缶幊痰乃枷搿?/p>
面向?qū)ο缶幊淌莻ゴ蟮木幊趟枷氩破剩彩钱?dāng)今主流的編程思想悠夯,它的問(wèn)題在于被過(guò)多的使用在其實(shí)并不需要使用它的情況下。
Swift是一門支持多編程范式的語(yǔ)言躺坟,既支持面向?qū)ο缶幊搪俨梗仓С置嫦騾f(xié)議編程,同時(shí)還支持函數(shù)式編程咪橙。在項(xiàng)目開發(fā)過(guò)程中夕膀,控制器和視圖部分由于使用系統(tǒng)框架,應(yīng)更多采用面向?qū)ο缶幊痰姆绞矫勒欤欢P突驑I(yè)務(wù)邏輯等自定義類型部分产舞,則應(yīng)優(yōu)先考慮面向協(xié)議編程。
PS. 這篇文章的寫作過(guò)程持續(xù)了很長(zhǎng)時(shí)間菠剩,中間幾乎夭折易猫,最后還是盡量將它寫完整(其實(shí)后半部分寫的很水)。面向協(xié)議編程是一個(gè)比較新的概念具壮,目前只是隱約可以看出它的一些長(zhǎng)處(在一些使用面向?qū)ο缶幊滩⒉惶m合的地方)准颓,不過(guò)蘋果已經(jīng)在自身框架中開始使用了,并確實(shí)改善了系統(tǒng)一些類型和方法的使用棺妓。
參考資料
- Protocol-Oriented Programming in Swift
- Protocol Oriented Programming
- Protocol-Oriented Programming is Object-Oriented Programming
- Heterogeneous vs Homogeneous Containers in Swift
- If You're Subclassing, You're Doing It Wrong
- 推薦必讀文章:多范式編程語(yǔ)言-以 Swift 為例
最后攘已,讓我們記住這張圖:(Quiz: Who is Crusty at Apple?)