/** * 謹獻給Yoyo * * 原文出處:https://www.toptal.com/swift/introduction-protocol-oriented-programming-swift * @author dogstar.huang <chanzonghuang@gmail.com> 2016-12-06 */
協議是Swift編程語言中一個很是強大的特性。html
協議用於定義「符合某個指定任務或者功能片的方法藍圖,屬性,以及其餘要求」。程序員
Swift在編譯時檢查協議一致性問題,使得開發者能夠在運行程序前發現代碼中的一些致命錯誤。協議使得開發者能夠在Swift編寫靈活和可擴展的代碼而不用妥協該語言的表現力。編程
Swfit經過提供一些最多見奇怪問題的解決方案以及許多其餘編程語言的接口限制,進一步得到了使用協議的便利性。
swift
經過面向協議編程,編寫靈活、可擴展的Swfit代碼。數組
在早期的Swfit版本中,只能擴展類、結構以及枚舉類型,在不少現代編程語言裏也是這樣的。然而,自從Swift 2 開始,也能對協議進行擴展了。安全
此文章會考察在Swfit中協議如何用於編寫可重用、可維護的代碼,以及經過使用協議擴展如何修改可以讓單個小模塊合併成一個大型面向協議的代碼庫。數據結構
什麼是協議?app
在其最簡單的定義裏,協議是指描述某些屬性和方法的接口。任何符合協議的類型,都應該使用合適的值填充協議中定義的特定屬性,而且實現其必要的方法。例如:編程語言
protocol Queue { var count: Int { get } mutating func push(_ element: Int) mutating func pop() -> Int }
此Queue協議描述了一個隊列,它包含了整型的元素。此語法至關明瞭。spa
在協議塊裏面,當描述某個屬性時,咱們必須指定該屬性是隻讀{ get }
仍是既可讀又可寫{ get set }
。這裏,變量Count(類型爲Int
)是隻讀的。
若是某個協議要求有一個讀寫的屬性,那就不能用一個存放常量的屬性或者一個只讀的計算值來填充。
若是協議只要求屬性是可讀的,那麼能夠是任意類型的屬性,而且該屬性也能夠是可寫的,若是這對於你的代碼有用的話。
對於在協議裏定義的方法,使用關鍵詞mutating
指明該方法將會改變上下文是很是重要的。除此以外,方法的簽名足以做爲定義。
爲了符合協議,類型必須提供所有實例屬性以及實如今協議中描述的所有方法。例以下面,是一個符合咱們Queue
協議的Container
結構。此結構本質上保存壓入的Int
到一個私有的items
數組。
struct Container: Queue { private var items: [Int] = [] var count: Int { return items.count } mutating func push(_ element: Int) { items.append(element) } mutating func pop() -> Int { return items.removeFirst() } }
然而,咱們當前的Queue協議有一堆缺點。
僅有處理Int
的容器才能符合此協議。
咱們能夠經過使用「關聯類型」特性來去掉這個限制。關聯類型的工做方式相似泛型。爲了演示,讓咱們修改Queue協議以採用關聯類型:
protocol Queue { associatedtype ItemType var count: Int { get } func push(_ element: ItemType) func pop() -> ItemType }
如今此Queue協議容許儲存任意類型的元素了。
在這個Container
結構的實現裏,編譯器會根據上下文(例如方法返回類型和參數類型)決定此關聯類型。這種方式使得咱們能夠建立一個帶有泛型元素的Container
結構。例如:
class Container<Item>: Queue { private var items: [Item] = [] var count: Int { return items.count } func push(_ element: Item) { items.append(element) } func pop() -> Item { return items.removeFirst() } }
在不少狀況下,使用協議均可以簡化代碼的編寫。
例如,任何表示錯誤的對象都會符合Error
(或者LocalizedError
,以防咱們想提供本地化的描述)協議。
而後在你的代碼裏,處理錯誤的相同邏輯就能夠應用到任意這些錯誤對象上。所以,你不須要使用任何指定的對象來表示錯誤,用任何符合Error
或LocalizedError
協議的就能夠了。
你甚至能夠擴展String類型,讓它符合LocalizedError
協議而且把字符串看成錯誤拋出。
extension String: LocalizedError { public var errorDescription: String? { Return NSLocalizedString(self, comment:」」) } } throw 「Unfortunately something went wrong」 func handle(error: Error) { print(error.localizedDescription) }
協議擴展基於協議自己的威力。這使得咱們能夠:
讓咱們再來建立一個協議:
rotocol ErrorHandler { func handle(error: Error) }
這個協議描述了負責處理應用中出現的錯誤的對象。例如:
struct Handler: ErrorHandler { func handle(error: Error) { print(error.localizedDescription) } }
這裏咱們只是打印了錯誤的本地化描述。使用協議擴展咱們可讓這個實現成爲默認的實現。
extension ErrorHandler { func handle(error: Error) { print(error.localizedDescription) } }
經過提供一個默認的實現,這樣使得handle
方法變成可選。
使用默認的行爲來擴展已經存在的協議的能力至關強大,這使得協議能夠發展和擴展,而不須要擔憂破壞既有代碼的兼容性。
咱們已經提供了handle
方法的默認實現,但打印到控制檯對於終端用戶一點用都沒有。
當錯誤處理器是一個視圖控制器時,咱們可能更傾向經過本地化描述來顯示某些排序好的警告視圖給他們看。爲了作到這一點,能夠擴展ErrorHandle
協議,但限制此擴展只用於既定的場景(例如,當類型是視圖控制器時)。
Swift允使咱們使用where
關鍵字添加這樣的條件到協議擴展裏。
extension ErrorHandler where Self: UIViewController { func handle(error: Error) { let alert = UIAlertController(title: nil, message: error.localizedDescription, preferredStyle: .alert) let action = UIAlertAction(title: "OK", style: .cancel, handler: nil) alert.addAction(action) present(alert, animated: true, completion: nil) } }
在上面代碼片斷裏的Self(「S」大寫)是指類型(結構、類或枚舉)。經過這樣指定後咱們只能爲繼承於UIViewController
的類型擴展此協議,咱們可使用UIViewController
指定方法(例如present(viewControllerToPresnt: animated: completion)
)。
如今, 任何符合ErrorHandler
協議的視圖控制器都擁有了handle
方法的默認實現,即顯示帶有本地化描述的警告視圖。
假設這裏有兩個協議,兩個都有一個方法,而且簽名同樣。
protocol P1 { func method() //some other methods } protocol P2 { func method() //some other methods }
這兩個協議都擴展了這個方法的默認實現。
extension P1 { func method() { print("Method P1") } } extension P2 { func method() { print("Method P2") } }
假設這裏有一個類型,符合這兩個協議。
struct S: P1, P2 { }
在這裏,咱們遇到了一個問題:不明確的方法實現。此類型沒有清楚指明它應該使用哪一個方法的實現。結果,咱們獲得了一個編譯錯誤。爲了修復這點,咱們須要添加這個方法的實現到此類型裏。
struct S: P1, P2 { func method() { print("Method S") } }
許多面向對象編程語言困擾於圍繞歧義擴展定義解決方案的限制。經過允使程序員在編譯器快速失敗時取得控制權,Swift至關優雅地處理了這一點。
再來看多一眼Queue
這個協議。
protocol Queue { associatedtype ItemType var count: Int { get } func push(_ element: ItemType) func pop() -> ItemType }
每一個符合此Queue
協議的類型都有一個定義所存放元素數量的count
實例屬性。這讓咱們,除其餘事項外,能夠比較這樣的類型以決定哪一個更大。能夠經過協議擴展來添加這樣的方法。
extension Queue { func compare<Q>(queue: Q) -> ComparisonResult where Q: Queue { if count < queue.count { return .orderedDescending } if count > queue.count { return .orderedAscending } return .orderedSame } }
Queue
協議沒有描述這個方法是由於它和隊列功能不相關。
因此它不是協議方法的默認實現,而是一個新的「裝飾」所有符合Queue
協議的類型方法實現。沒有協議擴展咱們將不得不分別把這個方法添加到每一個類型上。
協議擴展和使用基類可能看起來至關類似,但使用協議擴展有幾點好處。包括但不限於:
除了擴展本身的協議,還能夠擴展來自Swift標準庫的協議。例如,若是想找到隊列集合裏的平均值,能夠經過擴展標準的Collection
協議來作到這一點。
由Swfit標準庫提供的序列化數據結構,其元素能夠經過索引下標進行遍歷和訪問,一般符合Collection
協議。經過協議擴展,能夠擴展所有這些標準庫數據結構或者有選擇性地只擴展一部分。
注意:此協議在Swfit 2.x 之前叫
CollectionType
,而在Swfit 3 裏已更名爲Collection
。
extension Collection where Iterator.Element: Queue { func avgSize() -> Int { let size = map { $0.count }.reduce(0, +) return Int(round(Double(size) / Double(count.toIntMax()))) } }
如今能夠統計任何隊列集合(Array
, Set
等)的平均值了。沒有協議擴展的話,咱們須要爲每一個集合類型分別添加這個方法。
在Swfit標準庫裏,協議擴展用於實現,例如,這樣的方法諸如:map
, filter
, reduce
等。
extension Collection { public func map<T>(_ transform: (Self.Iterator.Element) throws -> T) rethrows -> [T] { } }
正如我曾經說過的,協議擴展使得咱們能夠爲某些方法添加默認實現,也能夠添加新的方法實現。但這兩種特性有什麼區別呢?讓咱們回到前面的錯誤處理器,找出答案。
protocol ErrorHandler { func handle(error: Error) } extension ErrorHandler { func handle(error: Error) { print(error.localizedDescription) } } struct Handler: ErrorHandler { func handle(error: Error) { fatalError("Unexpected error occurred") } } enum ApplicationError: Error { case other } let handler: Handler = Handler() handler.handle(error: ApplicationError.other)
結果是一個致命錯誤。
如今刪除聲明在協議裏的handle(error: Error)
方法。
protocol ErrorHandler { }
結果仍是同樣:一個致命錯誤。
這是否是意味着,爲協議方法添加默認實現和爲協議添加新的方法實現,沒有什麼不一樣?
不!仍是有區別的,若是把handler
變量的類型從Handler
改爲ErrorHandler
,你就能出來了。
let handler: ErrorHandler = Handler()
如今輸出到控制檯的是:The operation couldn’t be completed. (ApplicationError error 0.)
但若是把handle(error: Error)
這個方法還原到協議的聲明裏的話,結果又會變回到致命錯誤。
protocol ErrorHandler { func handle(error: Error) }
一塊兒來看下在各個場景中依次發生了什麼。
當協議存在方法聲明時:
協議聲明瞭handle(error: Error)
方法而且提供了一個默認的實現。此方法在Handler
實現中被重載。因此,此方法的正確實現將會在運行時被調用,不論是什麼類型的變量。
當協議不存在方法聲明時:
由於這個方法沒有聲明在協議裏,類型不能對它進行重載。那就是爲何一個被調用的方法的實現依賴於變量的類型。
若是變量的類型是Handler
,來自該類型的方法實現將會被調用。若是變量的類型是ErrorHandler
,來自該協議擴展的方法將會被調用。
在這篇文章中,咱們演示了一些在Swift裏協議擴展的強大之處。
不像其餘使用接口的編程語言,Swift沒有用沒必要要的限制來約束協議。Swift經過容許開發人員根據須要解決歧義來解決這些編程語言常見的問題。
使用Swift協議和協議擴展,能夠編寫出像大部分動態編程語言富有表現力且在編譯時類型安全的代碼。這使得你能夠確保代碼的可重用性和可維護性,以及在對Swift應用代碼庫作出修改時更有自信。
咱們但願這篇文章對你有所幫助,同時也歡迎評論、留言、反饋或者進一步的看法。
------------------------