Swift 程式語言 — Generics(3)

讓我們看看如何在泛型中使用 where 子句吧!

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

--

Photo by Andrew Neel on Unsplash

前言:

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

|泛型 Where 子句

如類型約束中所述,類型約束使你可以定義與泛型函數,下標或類型關聯的類型參數要求。

定義關聯類型的要求也很有用,你可以透過定義泛型 where 子句來做到。泛型 where 子句使你能夠要求關聯類型必須遵循某個協議,或者某些類型參數和關聯類型必須相同。泛型 where 子句以 where 關鍵字開頭,後面接著關聯類型的約束或關聯類型之間的相等關係。你可以在類型或函數主體的大括號前寫一個泛型 where 子句。

下面的範例定義了一個名為 allItemMatch 的泛型函數,該函數檢查兩個 Container 實例是否包含相同順序的相同項目。如果所有項目都匹配,則該函數返回 Booltrue,否則返回 false

要被檢查的兩個容器不必是相同類型的容器(雖然它可以是),但是它們必須容納相同類型的項目。透過類型約束和泛型 where 子句的組合來表達此要求:

該函數接收兩個參數 someContaineranotherContainer。其中 someContainer 參數類型為 C1,而 anotherContainer 參數類型為 C2C1C2 都是用於在調用函數時確定兩個容器類型的類型參數。

以下是對於函數的兩個類型參數的要求:

  • C1 必須遵循 Container 協議(寫為 C1: Container)。
  • C2 也必須遵循 Container 協議(寫為 C1: Container)。
  • C1 的 Item 必須與 C2 的 Item 相同(寫為 C1.Item == C2.Item)。
  • C1 的項目必須遵循 Equatable 協議(寫為 C1.Item: Equatable)。

第一、二項要求在函數的類型參數列表中定義,而第三、四項的要求在函數的泛型 where 子句中定義。

這些要求意味著:

  • someContainerC1 類型的容器。
  • anotherContainerC2 類型的容器。
  • someContaineranotherContainer 包含相同類型的項目。
  • 可以使用不等於運算符(!=)檢查 someContainer 中的項目,來查看它們是否彼此不同。

第三、四個要求結合後,意味著還可以使用 != 運算符檢查 anotherContainer 中的項目,因為它們與 someContainer 中的項目類型完全相同。

這些要求使 allItemsMatch(_:_:) 函數可以比較兩個容器,即使它們是不同的容器也是如此。

allItemsMatch(_:_:) 函數透過檢查兩個容器包含相同數量的項目開始。如果它們包含不同數量的項目,則無法匹配它們,並且函數返回 false

進行此檢查後,該函數使用 for-in 循環和半開範圍運算符(..<)對 someContainer 中的所有項目進行迭代。對於每個項目,該函數都會檢查 someContainer 中的項目是否不等於 anotherContainer 中的相應項目。如果兩個項目不相等,則兩個容器不匹配,該函數返回 false

如果循環結束而沒有找到不匹配項目,則兩個容器匹配,並且該函數返回 true

allItemsMatch(_:_:) 實際效果如下:

上方的範例創建一個 Stack 實例來存儲 String 值,並將三個 String 壓入堆棧。該範例還創建一個 Array 實例,該實例用一個 Array 常數初始化,該常數包含與堆棧相同的三個 String。即使堆棧和數組的類型不同,它們都遵循 Container 協議,並且都包含相同類型的值。因此,你可以使用這兩個容器作為參數來調用 allItemsMatch(_:_:) 函數。在上面的範例中,allItemsMatch(_:_:) 函數正確的回報兩個容器中的所有項目都匹配。

|具有泛型 Where 子句的擴展

你還可以使用泛型 where 子句作為擴展的一部分。下面的範例擴展了之前的泛型的 Stack 結構,以添加 isTop(_:) 方法。

這個新的 isTop(_:) 方法首先檢查堆棧是否為空,然後將給定的項目與堆棧中最上方的項目進行比較。如果嘗試在沒有泛型 where 子句的情況下執行此操作,則會遇到問題:isTop(:_) 的實現使用 == 運算符,但是 Stack 的定義不需要其項目可相等,因此使用 == 運算符會導致編譯時錯誤。使用泛型 where 子句讓你添加新的要求,以便擴展僅在堆棧中的項目為可相等時才添加 isTop(_:)

這裡是 isTop(:_) 方法實際效果如下:

如果你嘗試在元素為不可相等的堆棧上調用 isTop(_:) 方法,則會出現編譯時錯誤。

你可以使用帶有協議擴展的泛型 where 子句。下面的範例擴展了 Container 協議,以添加 startsWith(_:) 方法:

startsWith(_:) 方法首先確保容器中至少有一個項目,然後檢查容器中的第一個項目是否與給定的項目匹配。這個新的 startsWith(_:) 方法可以用於符合 Container 協議的任何類型,包括上面使用的堆棧和數組,只要容器的項目是可相等的即可。

上面範例中的泛型 where 子句要求 Item 遵循協議,但是你也可以編寫一個泛型的 where 子句,要求 Item 為特定類型。例如:

這個範例向 Item 類型為 Double 的容器添加 average() 方法。其迭代容器中的項目來將其相加,然後除以容器中的數量來計算平均值。它將計數從 Int 顯示轉換為 Double 來能夠進行浮點數除法。

你可以在擴展的一部分的泛型 where 子句包含多個需求,就像你在其他地方編寫的泛型 where 子句一樣。用逗號分隔列表中的每個要求。

|具有泛型 where 子句的關聯類型

你可以在關聯類型上包含泛型 where 子句。例如,假設你想要創建一個包含迭代器(Iterator)的 Container 版本,像是 Sequence 協議在標準庫中使用的版本。這是你的寫法:

迭代器上的泛型 where 子句要求迭代器必須便利容器項目中具有項目類型的元素,而不管迭代器的類型。透過 makeIterator() 函數可以訪問容器的迭代器。

對一個從另一個協議繼承的協議,可以透過在協議宣告包含泛型 where
子句來對繼承的關聯類型添加約束。例如,下面程式碼宣告了 ComparableContainer 協議,該協議要求 Item 符合 Comparable

|泛型下標

下標是可以被泛型的,並且它們可以包含泛型 where 子句。你在 subscript 後的尖括號內編寫佔位符類型名稱,並在下標正文的左大括號前寫一個泛型 where 子句。例如:

容器協議的這個擴展添加了一個下標,該下標採用索引序列,並且返回一個包含每個給定索引項目的陣列。該泛型下標受以下約束:

  • 尖括號中的泛型參數的 Indices 必須遵循標準庫中的 Sequence 協議的類型。
  • 下標採用單個參數 indices,該參數是 Indices 類型的實例。
  • 泛型 where 子句要求序列的迭代器必須遍歷 Int 類型的元素。這樣可以確保序列中的索引與用於容器的索引具有相同的類型。

總而言之,這些約束意味著索引參數傳遞的值是整數序列。

--

--

Jeremy Xue
Jeremy Xue ‘s Blog

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