Swift 程式語言 — Generics(2)

讓我們來看看什麼是 Associated Types 吧。

Jeremy Xue
Jeremy Xue ‘s Blog
7 min readJan 20, 2020

--

Photo by Perry Grone on Unsplash

前言:

如果還沒有看過泛型相關概念的讀者,可以參考我的上一篇文章:

|Associated Types

當定義協議時,定義一個或多個關聯類型(Associated types)作為協議定義的一部分有時很有用。關聯類型給協議中使用到的類型一個佔位符名稱。在協議被採用之前,沒有被指定用於關聯類型的實際類型。關聯類型使用 associatedtype 關鍵字來指定的。

|關聯類型的操作

這是一個名為 Container 的協議範例,該協議宣告了一個叫做 Item 的關聯類型:

Container 協議定義了任何容器必須提供的三個功能:

  • 它必須可以使用 append(_:) 方法來添加新項目到容器中。
  • 它必須可以透過返回 Int 值的 count 屬性來訪問容器中項目的數量。
  • 它必須可以使用帶有 Int 的索引值的下標來檢索每個容器中的項目。

這個協議沒有指定容器中的物品應該被如何儲存或它們允許使用的類型。該協議只有指定任何類型必須提供的三個功能點才能被視為一個 Container。只要滿足這三個要求,遵循的類型就可以提供其他功能。

遵循 Container 協議的任何類型都必須能夠指定其存儲的值的類型。具體來說,它必須確保只有正確類型的項目能被添加到容器中,並且必須清楚其下標返回的類型。

為了定義這些要求,Container 協議需要一個方法來引用容器中將要容納的元素,而無需知道特定容器的類型。容器協議需要指定傳遞給 append(_:) 方法的任何值必須與容器元素類型具有相同的類型,並且容器的下標返回的值必須與容器的元素類型具有相同的類型。

為此,Container 協議定義了一個稱為 Item 關聯類型,寫為 associatedtype Item。該協議沒有定義 Item 是什麼,該訊息留給任何遵循類型來提供。儘管如此,Item 別名還提供一個方式來引用 Container 中的項目類型,並且定義一個與 append(_:) 方法和下標一起使用的類型,來確保任何 Container 的預期行為是被強制的。

這是一個非泛型版本的 IntStack,適用於遵循 Container 協議:

IntStack 類型實現了 Container 協議的所有三個要求,並且在每個情況下都包裝了 Intstack 類型的現有功能的一部分來滿足這些要求。

此外,IntStack 為了 Container 的這個實現,指定要使用的合適 ItemInt 類型。typealias Item = Int 的定義將 Item 的抽象類型轉換為具體的 Int 類型。

由於 Swift 的類型推斷,你不需要實際的宣告一個 IntItem 作為 IntStack 定義的一部分。因為 IntStack 遵循了所有 Container 的所有要求,因此 Swift 只需查看 append(_:) 方法的 item 參數的類型與下標的返回類型,即可推斷合適的 Item 使用。確實,如果從上面的程式碼刪除了 typealias Item = Int 這行,那麼一切仍然有效,因為很明顯 Item 應該使用哪種類型。

你還可以使用泛型 Stack 類型符合 Container 協議:

這次,類型參數 Element 被作為 append(_:) 方法的 item 參數的類型以及下標的返回類型。因此,Swift 可以推斷 Element 是用作此特定容器的 Item 的合適類型。

|擴展現有類型來指定關聯值

你可以擴展現有類型來添加對協議的一致性,如 Adding Protocol Conformance with an Extension 中所述。這包括具有關聯類型的協議。

Swift 中的 Array 類型已經提供了 append(_:) 方法,count 屬性以及帶有 Int 索引的下標來檢索其元素。這三個功能皆符合 Container 協議的要求。這意味著你只需要宣告 Array 採用協議即可擴展 Array 來使其遵循 Container 協議。你可以使用一個空的擴展來達成此操作,如 Declaring Protocol Adoption with an Extension 中所述:

Array 現有的 append(_:) 方法和下標,Swift 可以推斷要使用於 Item 的合適類型,如同上面的泛型 Stack 類型一樣。定義此擴展後,可以將任何 Array 作為 Cotainer

|添加約束到關聯類型

你可以類型添加約束到協議中的關聯類型,來要求符合類型的類型滿足這些約束。例如,以下的程式碼定義了 Container 的版本,該版本要求容器中的 item 是相等的(equatable)。

為了遵循此版本的 Container,容器 Item 類型必須符合 Equatable 協議。

|在關聯類型的約束中使用協議

協議可以作為其自身要求的一部分出現。例如,這裡是一個優化 Container 協議的協議,添加了 suffix(_:) 方法的要求。suffix(_:) 方法從容器的最後返回給定數量的元素,並將他們存儲在 Suffix 類型的實例中。

在這個協議中,Suffix 是關聯類型,像是上面 Container 範例中的 item 類型。Suffix 具有兩個約束:其必須符合 SuffixableContainer 協議(目前正在定義的協議),並且 Item 類型必須與容器的 Item 類型相同。對 Item 的約束是通用的 where 子句,會在後面的 “具有泛型的 where 子句的關聯類型” 中說明。

這是上面的泛型類型的 Stack 類型的擴展,它增加了對 SuffixableContainer 協議的一致性:

在上面的範例中,StackSuffix 關聯類型也是 Stack,因此 Stack 上的後綴操作返回另一個 Stack。另外,符合 SuffixableContainer 的類型可以具有與自身不同的 Suffix 類型,這意味著後綴操作可以返回不同的類型。舉例,這是個對非泛型 IntStack 類型的擴展,其添加了 SuffixableContainer 一致性,使用 Stack<Int> 作為其後綴類型而不是 IntStack

--

--

Jeremy Xue
Jeremy Xue ‘s Blog

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