Swift 程式語言 — Protocols (2)
讓我們看看如何讓協議作為一個類型,並透過它使用委任模式。
前言:
若是對於協議還沒有概念的讀者,可以先參考我上一篇文章:
|協議作為類型
協議本身實際上不會實現任何功能。但是,你可以將協議作為程式碼中的完整類型。使用協議作為類型有時被稱為存在類型(Existential Type),它來自於短語 “存在類型 T,使得 T 遵循協議”。
你可以在允許使用其他類型的許多地方使用協議,包括:
- 作為函數,方法或初始化器中的參數類型或返回類型。
- 作為常數、變數或屬性的類型。
- 作為陣列、字典或其他容器中項目的類型。
因為協議是類型,所以其名稱以大寫字母作為開頭(例如 FullyNamed 和 RandomNumberGenerator),來匹配 Swift 中其他類型的名稱(例如 Int、String 和 Double)。
這是用協議作為類型的範例:
這個範例定義了一個稱為 Dice
的 class
,該 class
表示用於棋盤遊戲的 n 面骰子。Dice
實例具有一個稱為 sides
的 Int
屬性,該屬性表示他們具有多少面,還有一個名為 generator
的屬性,該屬性提供一個隨機數生成器,用於創建擲骰值。
generator
屬性為 RandomNumberGenerator
。因此,可以將其設置為遵循 RandomNumberGenerator
協議的任何類型實例。分配給該屬性的實例不需要任何其他操作,只是該實例必須遵循 RandomNumberGenerator
協議。
由於該類型為 RandomNumberGenerator
,因此 Dice
class
中的程式碼只能以適用於所有遵循此協議的生成器方式與生成器交互。這意味著它不能使用由生成器的基礎類型定義的任何方法或屬性。但是你可以像從向下轉換中討論的那樣,從協議類型向下轉換(Downcasting)為基礎類型的方式,就像從 superclass 向下轉換為 subclass 的方式一樣。
Dice
還有一個初始化器,用於設置其初始值。此初始化器具有一個 generator
的參數,該參數類型也為 RandomNumberGenerator
。初始化新的 Dice
實例時,可以在該參數中傳遞任何符合類型的值。骰子提供了一個實例方法 roll
,該方法返回 1 到骰子面數之間的整數值。該方法調用了 generator
的 random()
方法來創建介於 0.0 ~ 1.0
之間的新隨機數,並使用該隨機數來創建正確範圍內的擲骰值。由於已知 generator
會遵循 RandomNumberGenerator
,因此可以確保使用 random()
方式進行調用。
下面介紹如何使用 Dice
class
創建一個具有 LinearCongruentialGenerator
實例作為其隨機數生成器的六面骰子:
看看結果:
|委託
委託(Delegation)是一種設計模式,使 class 或 struct 可以將某些職責交給(或委託)其他類型的實例。透過定義封裝委託職責的協議來實現此設計模式,從而確保遵循類型(所謂的委託)提供已委託的功能。委託可用於響應特定操作或從外部來源取回數據而不需要暸解來源具體的基礎類型。
下面的範例定義了兩種用於基於骰子的棋盤遊戲的協議:
DiceGame
協議是可以被涉及骰子的任何遊戲採用的協議。
可以採用 DiceGameDelegate
協議來追蹤 DiceGame 的進度。為了防止強引用循環(Strong reference cycles),將委託宣告為弱引用(weak reference)。有關弱引用的資訊,可參考 Strong Reference Cycles Between Class Instances。將協議標記為 class-only,可使之後的 SnakesAndLadders class 宣告其委託時必須使用弱引用。如 Class-Only Protocols 中所述,class-only 的協議透過從其 AnyObject
的繼承來標記。
這裡是最初在 Control Flow 中引入的 Snakes and Ladders game 遊戲版本。此版本於將 Dice
實例用於其骰子;採用 DiceGame
協議;並將其進度通知 DiceGameDelegate
:
有關 Snakes and Ladders 的遊戲方式,請參考 Break 。
該遊戲版本包裝名為 SnakesAndLadders
的 class
,該 class
採用 DiceGame
協議。它提供了一個可讀的 dice
屬性和一個 play()
方法來遵循該協議。(dice
屬性被宣告為常數屬性,因為初始化之後不需要更改它,並且協議僅要求他必須為可讀的。)
Snakes and Ladders 遊戲板的設置在該 class
的 init()
初始化器中進行。所有遊戲邏輯都移動到協議的 play
方法中,該方法使用協議的必須 dice
屬性來提供擲骰值。
請注意,delegate
屬性被定義為 optional 的 DiceGameDelegate
,因為玩遊戲不需要委託。由於它是 optional 類型,所以 delegate
屬性會自動設置為初始值 nil
。之後,遊戲實例化器可以選擇將屬性設置為合適的委託人。因為 DiceGameDelegate
協議是 class-only 的,所以你可以宣告 delegate
是 weak
來防止循環引用。
DiceGameDelegate
提供了三種追蹤遊戲進度的方法。這三種方法已合併到上面 play()
方法中的遊戲邏輯中,並在新遊戲開始、新回合開始或遊戲結束時被調用。
由於 delegate
屬性為 optional 的 DiceGameDelegate
,因此 play()
方法每次在 delegate
上調用方法時都使用 optional chaining。如果 delegate
為 nil
,則這些委託調用將優雅的失敗並且沒有錯誤。如果 delegate
屬性不為 nil
,則調用 delegate
方法,並將其作為參數傳遞給 SnakesAndLadders
實例。
下一個範例展示了一個名為 DiceGameTracker
的 class
,該 class
採用 DiceGameDelegate
協議:
DiceGameTracker
實現 DiceGameDelegate
所需要的三種方法,他使用這些方法來追蹤遊戲進行的回合數。在遊戲開始時,他將 numberOfTurns
屬性重置為 0
,在每次新的回合開始時將其遞增,並在遊戲結束時印出總回合數。
而上面的 gameDidStart(_:)
的實現使用 game
參數來印出有關遊玩的遊戲的一些介紹訊息。game
參數類型為 DiceGame,而不是 SnakesAndLadders
,因此 gameDidStart(_:)
只能訪問和使用作為 DiceGame
協議的一部分實現的方法和屬性。但是該方法仍然可以使用類型轉換來查詢基礎實例的類型。在這個範例中,它檢查遊戲是否實際上為幕後的 SnakesAndLadders
實例,如果是,則印出適當的訊息。
gameDidStart(_:)
方法還訪問傳遞的 game
參數的 dice
屬性。由於已知遊戲遵循 DiceGame
協議,因此保證具有 dice
屬性,因此 gameDidStart(_:)
方法可以訪問和打印 dice
的 sides
屬性,而不管正在玩哪種遊戲。
DiceGameTracker
的運作方式如下:
結果如下:
|通過擴展添加協議一致性
即使你無權訪問現有類型的原始碼,也可以擴展現有類型來採用並遵循新協議。擴展可以向現有類型添加新的屬性、方法與下標,因此可以添加協議可能要求的任何要求。有關擴展的更多訊息,可以參考 Extensions。
當擴展中的一制性添加到實例屬性時,該類型的現有實例會自動採用並遵守協議。
舉個例子,此協議稱為 TextRepresentable
,可以用任何一種可以表示為文本的類型來實現。這可能是對自身的描述,也可能是當前狀態的文本版本:
上面的 Dice
class
可以用擴展來採用和遵循 TextRepresentable
:
這個擴展採用新協議的方式與 Dice
在其原始實現中提供新協議的方式完全在同。在類型名稱之後提供協議名稱,並用冒號分隔,並在擴展程序的花括號內提供協議所有要求的實現。
現在,任何 Dice
實例都可以視為 TextRepresentable
:
相同的,可以擴展 SnakesAndLadders
遊戲 class
來採用並遵循 TextRepresentable
協議:
|有條件的遵循協議
泛型僅在某些條件下(例如:當類型的泛型參數遵循協議時)滿足一個協議的要求。透過在擴展類型時列出約束,可以使泛型更有條件的符合協議。透過編寫通用的 where
子句,在要採用的協議名稱後寫下這些約束。有關通用的 where
子句的更多訊息,請參考 Generic Where Clauses。
下面擴展使 Array
實例在存儲符合 TextRepresentable
類型的元素時就符合 TextRepresentable
協議。
|宣告協議採用擴展
如果類型已經遵循協議的所有要求,但尚未宣告採用協議,則可以使它採用帶有空擴展名的協議:
現在,可以在需要 TextRepresentable
的任何類型中使用 Hamster
實例:
類型不會僅通過滿足協議要求就自動採用協議。他們必須始終明確宣告對其協議的採用。
|協議類型的集合
協議可作為要存儲在集合中的類型,例如陣列或字典,如 Protocols as Types 中所述。這個範例創建一個 TextRepresentable
的 things
陣列:
現在可以遍歷陣列中的項目,並且印出每個項目的文字說明:
查看結果:
請注意,thing
常數的類型為 TextRepresentable
。他不是 Dice
、DiceGame
或 Hamster
類型的,即使幕後的實際類型是其中一種。儘管如此,由於它的類型為 TextRepresentable
,並且以知 TextRepresentable
的任何內容都具有 textualDescription
屬性,因此每次循環都可以安全的訪問 thing.textualDescription
。