Swift 程式語言 — Protocols (2)

讓我們看看如何讓協議作為一個類型,並透過它使用委任模式。

Jeremy Xue
Jeremy Xue ‘s Blog
11 min readNov 10, 2019

--

Photo by Scott Webb on Unsplash

前言:

若是對於協議還沒有概念的讀者,可以先參考我上一篇文章:

|協議作為類型

協議本身實際上不會實現任何功能。但是,你可以將協議作為程式碼中的完整類型。使用協議作為類型有時被稱為存在類型(Existential Type),它來自於短語 “存在類型 T,使得 T 遵循協議”。

你可以在允許使用其他類型的許多地方使用協議,包括:

  • 作為函數,方法或初始化器中的參數類型或返回類型。
  • 作為常數、變數或屬性的類型。
  • 作為陣列、字典或其他容器中項目的類型。

這是用協議作為類型的範例:

這個範例定義了一個稱為 Diceclass,該 class 表示用於棋盤遊戲的 n 面骰子。Dice 實例具有一個稱為 sidesInt 屬性,該屬性表示他們具有多少面,還有一個名為 generator 的屬性,該屬性提供一個隨機數生成器,用於創建擲骰值。

generator 屬性為 RandomNumberGenerator。因此,可以將其設置為遵循 RandomNumberGenerator 協議的任何類型實例。分配給該屬性的實例不需要任何其他操作,只是該實例必須遵循 RandomNumberGenerator 協議。

由於該類型為 RandomNumberGenerator,因此 Dice class 中的程式碼只能以適用於所有遵循此協議的生成器方式與生成器交互。這意味著它不能使用由生成器的基礎類型定義的任何方法或屬性。但是你可以像從向下轉換中討論的那樣,從協議類型向下轉換(Downcasting)為基礎類型的方式,就像從 superclass 向下轉換為 subclass 的方式一樣。

Dice 還有一個初始化器,用於設置其初始值。此初始化器具有一個 generator 的參數,該參數類型也為 RandomNumberGenerator。初始化新的 Dice 實例時,可以在該參數中傳遞任何符合類型的值。骰子提供了一個實例方法 roll,該方法返回 1 到骰子面數之間的整數值。該方法調用了 generatorrandom() 方法來創建介於 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

該遊戲版本包裝名為 SnakesAndLaddersclass,該 class 採用 DiceGame 協議。它提供了一個可讀的 dice 屬性和一個 play() 方法來遵循該協議。(dice 屬性被宣告為常數屬性,因為初始化之後不需要更改它,並且協議僅要求他必須為可讀的。)

Snakes and Ladders 遊戲板的設置在該 classinit() 初始化器中進行。所有遊戲邏輯都移動到協議的 play 方法中,該方法使用協議的必須 dice 屬性來提供擲骰值。

請注意,delegate 屬性被定義為 optional 的 DiceGameDelegate,因為玩遊戲不需要委託。由於它是 optional 類型,所以 delegate 屬性會自動設置為初始值 nil。之後,遊戲實例化器可以選擇將屬性設置為合適的委託人。因為 DiceGameDelegate 協議是 class-only 的,所以你可以宣告 delegateweak 來防止循環引用。

DiceGameDelegate 提供了三種追蹤遊戲進度的方法。這三種方法已合併到上面 play() 方法中的遊戲邏輯中,並在新遊戲開始、新回合開始或遊戲結束時被調用。

由於 delegate 屬性為 optional 的 DiceGameDelegate,因此 play() 方法每次在 delegate 上調用方法時都使用 optional chaining。如果 delegatenil,則這些委託調用將優雅的失敗並且沒有錯誤。如果 delegate 屬性不為 nil,則調用 delegate 方法,並將其作為參數傳遞給 SnakesAndLadders 實例。

下一個範例展示了一個名為 DiceGameTrackerclass,該 class 採用 DiceGameDelegate 協議:

DiceGameTracker 實現 DiceGameDelegate 所需要的三種方法,他使用這些方法來追蹤遊戲進行的回合數。在遊戲開始時,他將 numberOfTurns 屬性重置為 0,在每次新的回合開始時將其遞增,並在遊戲結束時印出總回合數。

而上面的 gameDidStart(_:) 的實現使用 game 參數來印出有關遊玩的遊戲的一些介紹訊息。game 參數類型為 DiceGame,而不是 SnakesAndLadders,因此 gameDidStart(_:) 只能訪問和使用作為 DiceGame 協議的一部分實現的方法和屬性。但是該方法仍然可以使用類型轉換來查詢基礎實例的類型。在這個範例中,它檢查遊戲是否實際上為幕後的 SnakesAndLadders 實例,如果是,則印出適當的訊息。

gameDidStart(_:) 方法還訪問傳遞的 game 參數的 dice 屬性。由於已知遊戲遵循 DiceGame 協議,因此保證具有 dice 屬性,因此 gameDidStart(_:) 方法可以訪問和打印 dicesides 屬性,而不管正在玩哪種遊戲。

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 中所述。這個範例創建一個 TextRepresentablethings 陣列:

現在可以遍歷陣列中的項目,並且印出每個項目的文字說明:

查看結果:

請注意,thing 常數的類型為 TextRepresentable。他不是 DiceDiceGameHamster 類型的,即使幕後的實際類型是其中一種。儘管如此,由於它的類型為 TextRepresentable,並且以知 TextRepresentable 的任何內容都具有 textualDescription 屬性,因此每次循環都可以安全的訪問 thing.textualDescription

--

--

Jeremy Xue
Jeremy Xue ‘s Blog

Hi, I’m Jeremy. [好想工作室 — iOS Developer]