Swift 程式語言 — Generics(1)

讓我們一起來看看什麼是泛型吧!

Jeremy Xue
Jeremy Xue ‘s Blog
15 min readDec 9, 2019

--

Photo by Kelly Sikkema on Unsplash

前言:

泛型程式讓使你編寫靈活、可重用的函數與類型,這些可以根據你定義的要求與任何類型一起使用。你可以避免編寫重複的程式碼,並且以清晰抽象的方式表達其意圖。

泛型是 Swift 中最強大的功能之一,許多 Swift 中標準庫都是使用泛型程式碼建構的。事實上,你可能沒有意識到在整個「語言指南」中都使用了泛型。例如:Swift 中的 Array 和 Dictionary 類型都是泛型集合。你可以創建一個包含 Int 值的 Array 或是一個包含 String 值的數組,或者實際上是可以在 Swift 創建的任何其他類型的 Array。同樣的,你可以創建一個 Dictionary 來存儲任何指定類型的值,並且對該類型沒有任何限制。

|泛型解決的問題

這是一個標準的非泛型函數,稱為 swapTwoInts(_:_:),該函數交換兩個 Int 值:

此函數使用 in-out 參數來交換 ab 的值,如同 In-Out Parameters 中所述。

swapTwoInts(_:_:) 函數將 b 的原始值交換為 a,然後將 a 的原始值交換為 b。你可以調用此函數來交換兩個 Int 變數中的值:

swapTwoInts(_:_:) 函數很有用,但只能與 Int 值一起使用。如果要交換兩個 String 值或是兩個 Double 值,你就必須寫更多的函數,例如下面的 swapTwoStrings(_:_:)swapTwoDoubles(_:_:) 所表示:

你可能已經注意到 swapTwoStrings(_:_:)swapTwoDoubles(_:_:) 的主體是相同的。唯一的差別是它們接受值的類型(IntStingDouble)。
編寫一個交換任何類型的兩個值的函數會更有用,並且也更加靈活。泛型程式碼使你可以編寫這類的功能。(下面定義了這種功能的泛型版本。)

在三個函數中,a 和 b 的類型必須相同。如果 a 和 b 的類型不同,則無法交換它們的值。Swift 是一種類型安全的語言,並且不允許(例如) String 類型的變數和 Double 類型的變數相互交換值。嘗試這麼做會導致編譯時錯誤。

|泛型函數

泛型函數可以使用任何類型。這裡是一個 swapTwoInts(_:_:) 函數的泛型版本,稱為 swapTwoValues(_:_:)

swapTwoValues(_:_:) 函數的主體與 swapTwoInts(_:_:) 函數的主體相同。但是,swapTwoValues(_:_:) 的第一行與 swapTwoInts(_:_:) 略有不同,以下是第一行的比較:

該函數的泛型版本使用佔位符類型名稱(在這種情況下稱為 T)來代替實際的類型名稱(例如:Int, String, Double)。佔位符類型名稱沒有說明 T 必須是什麼,而是表示無論 T 代表什麼,ab 必須具有相同的 T 類型。每次調用 swapTwoValues(_:_:) 函數時,都要確定要代替 T 使用的實際類型。

泛型函數和非泛型函數的另一個區別是泛型函數的名稱 swapTwoValues(_:_:) 後跟著尖括號 <T> 的佔位符類型名稱 T。該括號告訴 Swift,TswapTwoValues(_:_:) 函數定義中的佔位符類型名稱。由於 T 是一個佔位符,所以 Swift 並不會查找稱為 T 的實際類型。

swapTwoValues(_:_:) 函數現在可以用與 swapTwoInts 相同的方式調用,除此之外還可以傳遞任何類型的兩個值,只要這兩個值彼此具有相同的類型即可。每次調用 swapTwoValues(_:_:) 時,都會從傳遞給函數的值的類型中推斷出作用於 T 的類型。

在下面的兩個範例中,T 分別判斷類型為 IntString

* NOTE
上面所定義的 swapTwoValues(_:_:) 函數受到名為 swap 的泛型函數啟發,後者是 Swift 標準庫的一部分,並自動提供在你的應用程序中。如果你需要在自己的程式碼中使用 swapTwoValues(_:_:) 函數的行為,則可以使用 Swift 現有的 swap(_:_:) 函數,而不必提供自己的實現。

|類型參數

上面的 swapTwoValues(_:_:) 範例中,佔位符類型 T 是類型參數的範例。類型參數指定並命名一個佔位符類型,並在函數名稱之後立即寫在一對匹配的尖括號中(例如 <T>)。

一旦指定了類型參數,就可以使用它來定義函數參數的類型(像是 swapTwoValues(_:_:) 函數的 ab 參數),或者作為函數的返回類型,又或是作為函數主體中的類型註解。在每種狀況下,每當調用函數時,類型參數都將替換為實際類型。(在上面的 swapTwoValues(_:_:) 範例中,第一次調用該函數時將 T 替換成 Int,而第二次調用時它將 T 替換成 String。)

你透過在尖括號內用逗號分隔多個類型參數名稱,可以提供多個類型參數。

|命名類型函數

在大多數的情況下,類型參數具有描述性名稱,例如 Dictionary<Key,Value> 中的 KeyValueArray<Element> 中的 Element,它向讀者介紹類型參數與其所使用的泛型類型或函數之間的關係。但是,當它們之間沒有有意義的關係時,通常使用單個字母,例如 TUV,又是例如上面 swapTwoValues(_:_:) 函數中的 T 來命名它們。

* NOTE
請始終為類型參數提供駝峰式大小寫名稱(例如 T 或 MyTypeParemeter)來表示它們是類型的佔位符,而不是值。

|泛型類型

除了泛型函數,Swift 還使你能夠定義自己的泛型類型。這些是可以與任何類型一起使用的自定義 class、struct 和 enum,類似於 Array 與 Dictionary。

本章節說明如何編寫稱為 Stack 的泛型集合類型。堆棧是一個有序的值,類似於 Array,但操作集比 Swift 的 Array 類型更受限制。Array 允許在 Array 中的任何位置插入和刪除新項目。但是,Stack 只允許將新項目附加到集合的最後(稱之為壓棧(push))。同樣的,堆棧僅允許刪除集合的最後一個項目(稱之為彈棧(pop))。

* NOTE
而堆棧的概念被 UINavigationController class 使用,來對其導航層次結構中對 viewController 進行建模。你調用 UINavigationController class 的 pushViewController(_:animated:) 方法將 viewController 添加(或推入)到導航堆棧中,並調用其 popViewControllerAnimated(_:) 方法從其導航堆棧中刪除(或彈出)viewController。每當你需要一個嚴謹的 “後進先出” 方法來管理集合時,堆棧是有用的集合類型。

下圖展示了堆棧的推入和彈出行為:

Picture from: https://docs.swift.org/swift-book/LanguageGuide/Generics.html
  1. 目前有三個值在堆棧中。
  2. 第四個值被推入到堆棧的最上方。
  3. 堆棧現在保有四個值,最近的一個在最上方。
  4. 堆棧中最上方的項目被彈出。
  5. 在彈出一個值後,堆棧再一次保有三個值。

這裡是如何編寫一個非泛型版本的堆棧。在這種情況下,是一個針對 Int 值的堆棧:

這個結構使用稱為 items 的 Array 屬性將值儲存到堆棧中。堆棧提供了兩個方法,分別是 pushpop ,它們用來在堆棧中推入和彈出值。這些方法都被標記為 mutating,因為它們需要修改(或變異)struct 中的 items

然而,上方展示的 IntStack 類型只能與 Int 值一起使用。定義一個泛型的堆棧 class,它可以管理任何值的堆棧,將會更加有用。

這裡是相同程式碼的泛型版本:

請注意, Stack 的泛型版本與非泛型版本基本上相同,但是具有一個稱為 Element 的類型參數來代替 Int 的實際類型。該類型參數被編寫到結構名稱後的一對尖括號 <Element> 中。

Element 為了之後要提供的類型定義了一個佔位符名稱。在結構定義內的任何位置,都可以將這種將來的類型稱為 Element。在這種情況下,Element 可以在三個位置作為佔位符:

  1. 創建一個名為 items 的屬性,該屬性使用類型為 Element 的空 Array 初始化。
  2. 要指定 push(_:) 的方法具有單個名為 item 的參數,該函數必須為 Element 類型。
  3. 指定 pop() 方法返回的值將會是 Element 類型的值。
  4. 因為它是泛型類型,所以 Stack 在 Swift 中可以被創建為任何有效類型的堆棧,類似於 Array 和 Dictionary的方式。

你可以創建一個 Stack 實例,透過在尖括號中寫入要儲存在堆棧中的類型。例如,要創建新的字串堆棧,請編寫 Stack<String>()

堆棧現在包含 4 個字符

這裡是 stackOfStrings 在推入這四個值到堆棧後看起來的樣子:

Picture from: https://docs.swift.org/swift-book/LanguageGuide/Generics.html

從堆棧中彈出一個值會刪除和返回最上方的值 "cuatro"

在彈出最上方值後,堆棧看起來的樣子:

Picture from: https://docs.swift.org/swift-book/LanguageGuide/Generics.html

|擴展泛型類型

當你擴展泛型類型時,你不會提供類型參數清單作為擴展定義的一部分。相反的,在擴展的主體中提供原始類型定義的類型參數列表是可用的,並且原始類型參數名稱被用於引用原始定義的類型參數。

下面的範例擴展了泛型的 Stack 類型,來添加一個名為 topItem 的唯讀計算屬性,該屬性返回堆棧中最上方的項目,但不會從堆棧中彈出:

topItem 屬性返回一個 Element 類型的可選值。如果堆棧為空,則 topItem 返回 nil;如果堆棧不為空,則 topItem 返回 items 中的最後一個項目。

請注意,此擴展沒有定義類型參數列表。相反的,擴展中使用 Stack 類型的現有類型參數名稱 Element 來表示 topItem 計算屬性的可選類型。

現在 topItem 計算屬性可以被用於任何 Stack 實例,用來訪問和查詢其最上方的項目,而不需將其移除。

泛型類型的擴展還可以包含擴展類型的實例必須滿足的條件才能獲得的新功能,在之後 Extensions with a Generic Where Clause 的部分會提及。

|類型約束

swapTwoValues(_:_:) 函數和 Stack 類型可以與任何類型一起使用。然而,有時候對泛型函數的類型和泛型類型上強制執行類型約束很有用。類型約束指定類型參數必須從特定的 class 繼承或遵循特定的協議或協議組合。

舉例來說,Swift 中的 Dictionary 類型對作用於其 key 的類型進行了限制。如 Dictionary 中所述,Dictionary 中的 key 的類型必須是 Hashable。也就是說,它必須提供一個使其自身具有獨特代表性的方式。Dictionary 需要其 key 是 hashable 來檢查它是否已幾包含特定 key 的值。沒有了這個要求,Dictionary 將無法確定是否應該為特定 key 插入或替換一個值,也無法為 Dictionary 中已經存在的特定 key 找到值。

透過對 Dictionary 中的 key 類型進行類型約束來強制這個要求,該約束指定 key 類型必須遵循 Hashable 協議,該協議是 Swift 標準庫中的特殊協議。默認情況下,所有 Swift 中的基本類型(例如:String、Int、Double 和 Bool)都是 hashable 的。

你可以在創建自定義泛型類型時定義自己的類型約束,並且這些約束提供強大的泛型編程能。像是 Hashable 這樣抽象的概念,根據類型的概念上的特徵而不是具體的類型來表徵類型。

|類型約束語法

你可以透過在類型參數名稱後放置一個單一的 class 或協議限制,使用冒號分隔,作為類型參數列表的一部分來編寫類型約束。泛型函數的類型約束的基本語法如下所示(僅管泛型類型的語法相同):

上面的假設函數有兩個參數。第一個參數 T 具有類型限制,該約束要求 TSomeClass 的 subclass。第二個參數 U 具有類型約束,該約束要求 U 遵循 SomeProtocol 協議。

|操作類型的操作

這是一個名為 findIndex(ofString:in:) 的非泛型函數,該函數給定要查找的 String 值和可以在其中查找的 String 值的 ArrayfindIndex(ofString:in:) 函數返回一個可選的 Int 值。如果有找到,該值將是 Array 中第一個匹配 Stringindex;如果沒有找到 String 則是 nil

findIndex(ofString:in:) 函數可被使用在 String Array 中查找 String

但是,在 Array 查找值 index 的原理不僅對 String 有用。你可以透過將任何提及的 String 替換成某個 T 類型的值來編寫與泛型函數相同的功能。

這裡是你可能期望寫出的 findIndex(ofString:in:) 的泛型版本,名為 findIndex(of:in:)。請注意,此函數的版本的返回類型仍為 Int?,因為該函數返回可選的 index 數字,而不是該 Array 中的可選值。請注意,由於範例後說明的原因,該函數無法編譯:

這個函數沒有照上面所編寫的編譯。而問題在於相等性檢查 if value == valueToFind。並非 Swift 中的每個類型都可以與等於運算符 == 進行比較。例如,如果你創建自己的 class 或 struct 來表示複雜的數據模型,則該 class 或 struct 的等於含義不是 Swift 可以為你猜測的。因此,無法保證此程式碼適用於每種可能的 T 類型,並在嘗試編譯該程式碼時會報出相應的錯誤。

但並不是沒有辦法。Swift 標準庫定義了一個名為 Equatable 的協議,該協議要求任何遵循的類型實現等於運算符和不等運算符來比較該類型的任意兩個值。所有 Swift 的標準類型都自動支援 Equatable 協議。

Equatable 的任何類型都可以與 findIndex(of:in:) 函數一起安全的使用,因為它保證支援等於運算符。為了表達這個事實,在定義函數時,你編寫 Equatable 的類型約束來作為類型參數定義的一部分:

findIndex(of:in:) 的單一函數類型寫為 T: Equatable,來表示 “任何遵循 Equatable 協議的類型 T”。

findIndex(of:in:) 函數現在可以成功編譯,並可以與任何 Equatable 類型一起使用,例如 DoubleString

--

--

Jeremy Xue
Jeremy Xue ‘s Blog

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