Swift 中面向協議編程的介紹
已發表: 2022-03-11協議是 Swift 編程語言的一個非常強大的特性。
協議用於定義“適合特定任務或功能的方法、屬性和其他要求的藍圖”。
Swift 在編譯時檢查協議一致性問題,允許開發人員在運行程序之前發現代碼中的一些致命錯誤。 協議允許開發人員在 Swift 中編寫靈活且可擴展的代碼,而不必損害語言的表達能力。
Swift 通過為困擾許多其他編程語言的接口的一些最常見的怪癖和限制提供變通方法,進一步提高了使用協議的便利性。
在早期版本的 Swift 中,只能擴展類、結構和枚舉,這在許多現代編程語言中都是如此。 然而,從 Swift 的第 2 版開始,擴展協議也成為可能。
本文探討瞭如何使用 Swift 中的協議來編寫可重用和可維護的代碼,以及如何通過使用協議擴展將大型面向協議的代碼庫的更改合併到一個地方。
協議
什麼是協議?
在最簡單的形式中,協議是描述一些屬性和方法的接口。 任何符合協議的類型都應該用適當的值填充協議中定義的特定屬性,並實現其必要的方法。 例如:
protocol Queue { var count: Int { get } mutating func push(_ element: Int) mutating func pop() -> Int }
Queue 協議描述了一個包含整數項的隊列。 語法非常簡單。
在協議塊內部,當我們描述一個屬性時,我們必須指定該屬性是只有 gettable { get }
還是同時 gettable 和 settable { get set }
。 在我們的例子中,變量 Count (類型為Int
)只能獲取。
如果協議要求屬性是可獲取和可設置的,那麼常量存儲屬性或只讀計算屬性無法滿足該要求。
如果協議只要求一個屬性是可獲取的,那麼任何類型的屬性都可以滿足該要求,並且該屬性也可設置是有效的,如果這對您自己的代碼有用的話。
對於協議中定義的函數,重要的是指示函數是否會使用mutating
關鍵字更改內容。 除此之外,函數的簽名足以作為定義。
為了符合協議,類型必須提供所有實例屬性並實現協議中描述的所有方法。 例如,下面是一個符合我們Queue
協議的 struct 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() } }
然而,我們當前的隊列協議有一個主要缺點。
只有處理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
,如果我們想提供本地化描述)協議。
然後可以將相同的錯誤處理邏輯應用於整個代碼中的任何這些錯誤對象。 因此,您不需要使用任何特定對象(如 Objective-C 中的 NSError)來表示錯誤,您可以使用任何符合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) }
協議擴展
協議擴展建立在令人敬畏的協議之上。 它們使我們能夠:
提供協議方法的默認實現和協議屬性的默認值,從而使它們“可選”。 符合協議的類型可以提供自己的實現或使用默認實現。
添加協議中未描述的附加方法的實現,並使用這些附加方法“裝飾”符合協議的任何類型。 這個特性允許我們為已經符合協議的多種類型添加特定的方法,而無需單獨修改每種類型。
默認方法實現
讓我們再創建一個協議:
protocol 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
方法的默認實現,但打印到控制台對最終用戶並沒有太大幫助。
在錯誤處理程序是視圖控制器的情況下,我們可能更願意向他們展示某種帶有本地化描述的警報視圖。 為此,我們可以擴展ErrorHandler
協議,但可以將擴展限制為僅適用於某些情況(即,當類型是視圖控制器時)。
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 標準庫擴展
除了擴展您自己的協議之外,您還可以從 Swift 標準庫中擴展協議。 例如,如果我們想找到隊列集合的平均大小,我們可以通過擴展標準Collection
協議來實現。
Swift 標準庫提供的序列數據結構,其元素可以通過索引下標進行遍歷和訪問,通常符合Collection
協議。 通過協議擴展,可以擴展所有此類標準庫數據結構或選擇性地擴展其中的一些。
注意:在 Swift 2.x 中以前稱為
CollectionType
的協議在 Swift 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
等)的平均大小。 如果沒有協議擴展,我們將需要將此方法分別添加到每個集合類型。
在 Swift 標準庫中,協議擴展用於實現例如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()
現在控制台的輸出是:操作無法完成。 (ApplicationError 錯誤 0。)
但是如果我們將handle(error:Error)方法的聲明返回給協議,結果會變回致命錯誤。
protocol ErrorHandler { func handle(error: Error) }
讓我們看看每種情況下發生的順序。
當協議中存在方法聲明時:
該協議聲明了handle(error: Error)
方法並提供了默認實現。 該方法在Handler
實現中被覆蓋。 因此,無論變量的類型如何,都會在運行時調用該方法的正確實現。
當協議中不存在方法聲明時:
因為方法沒有在協議中聲明,所以類型不能覆蓋它。 這就是為什麼調用方法的實現取決於變量的類型。
如果變量是Handler
類型,則調用該類型的方法實現。 如果變量的類型是ErrorHandler
,則調用協議擴展中的方法實現。
面向協議的代碼:安全而富有表現力
在本文中,我們展示了 Swift 中協議擴展的一些強大功能。
與其他具有接口的編程語言不同,Swift 不會對協議進行不必要的限制。 Swift 通過允許開發人員在必要時解決歧義來解決這些編程語言的常見怪癖。
使用 Swift 協議和協議擴展,您編寫的代碼可以像大多數動態編程語言一樣富有表現力,並且在編譯時仍然是類型安全的。 這使您可以確保代碼的可重用性和可維護性,並更有信心地更改您的 Swift 應用程序代碼庫。
我們希望這篇文章對您有用,並歡迎任何反饋或進一步的見解。