Skip to Content

理解 Swift Generics

泛型程式設計(Generic Programming) 是經典的程式設計典範之一,不論是老牌的 C++,還是潮潮的 TypeScript,都能一睹泛型的風采。近年來,程式設計吹的是 static typing 風,泛型又開始被廣泛討論。

本篇將簡單介紹泛型的背景,再來理解並學習 Swift 語言的泛型寫法。

(撰於 2017-05-08,基於 Swift 3.1)

Definition

想像一下,有個需求是要交換兩個變數儲存的值,現在欲交換的變數是 int type,因此實作了 void swapInt(*int, *int) 的函式;接下來要交換的是 double,又寫了 void swapFloat(*double, *double),但兩個函式實作幾乎一樣(交換指標指向的值),如果還有 floatchar 等其他 n 種 data types,就必須寫 n 個版本的實作。如果程式語言支援函式重載,可以把 function name 都改成 swap,降低函式調用端的複雜度,但依然沒解決重複的問題。

泛型程式設計(Generic Programming)目的就是「消弭因為不同資料型態,而重複實作相同的演算法」。維基百科寫得非常清楚:

… is a style of computer programming in which algorithms are written in terms of types to-be-specified-later that are then instantiated when needed for specific types provided as parameters

白話一點,就是「延遲型別決定的時間點,再利用對程式碼的解析結果,到實例化該程式片段時,才決定實際的型別」。

泛型本質上就是減少冗餘代碼,增加代碼重用,遵守「DRY」的程式設計典範。有名的 C++ 的標準模版庫(STL)就是利用 C++ template 模版泛型,定義 mapvectorsortbinary_search 等重要的資料結構與與演算法,讓這些演算法非常「generic」,泛用於各種 data types。

Swift 這個四處剽竊集大成的多典範語言,當然也少不了對泛型的支持。Standard Library 內的 ArrayDictionarySet 等資料結構也支持不同 data types,而根據泛型化的對象不同,Swift 可分為 Generic FunctionsGeneric Types。接下來分別介紹兩者。

Generic Functions

想像一下,有三種不同廠牌,功能外觀也非常不一樣的手機,隸屬不同 class,

class AppleIPhone7 {}
class SamsungNote7 {}
class AsusZenfone {}

有一個 call(_:) 函式,需傳入一個 phone 參數,才能開始打電話,但由於各廠牌差異過大,需要定義多個實作相同的函式,每支手機才可正常 call out,

func call(by phone: AppleIPhone7) {
  print("\(type(of: phone)) is calling")
}
func call(by phone: SamsungNote7) {
  print("\(type(of: phone)) is calling")
}
func call(by phone: AsusZenfone) {
  print("\(type(of: phone)) is calling")
}

透過 Generic Functions 可以將 call(_:) 改成支援任何 data types 的泛型版本,

func call<Phone>(by phone: Phone) {
  print("\(type(of: phone)) is calling")
}

跟在 function name 後的 <Phone> 就是 Swift 的泛型語法,角括號 <> 內的 Phone 是虛擬的 type,官方名稱為 Type Parameters。這個 <Phone> 泛型有幾個特性:

  • 只是一個「代名詞」,不代表任何實際的型別。
  • 直接寫在該宣告名稱的後面(例如 function name 之後)。
  • 在該宣告的 body 中,可自由運用該泛型型別,例如傳入其他 function,或當作 return type。
  • 在可以確定實際調用的型別後,虛擬的泛型型別將會被真實型別取代。

泛型就是一個把代名詞換成已知名詞的概念,交由 compiler 代勞罷了。

Type Parameter 可使用任意的合法 identifier,常見如 <T><U>,或是與語彙環境有關,如 generic collection protocol 就用 <Element>,慣例會用大寫開頭,表示是一個 type,而非 value。

Generic Types

除了 function 以外,Swift 的 first class citizens(struct、enum、class)、initializers,以及 typelias 都支援泛型宣告功能,這裡直接使用官方的 Stack 例子:

struct Stack<Element> {                   // 1
  var items = [Element]()                 // 2
  mutating func push(_ item: Element) {   // 3
    items.append(item)
  }
  mutating func pop() -> Element {        // 4
    return items.removeLast()
  }
}

範例宣告了一個 Stack struct,帶有一個 type parameter「Element」(1),Element 這個虛擬的 type 可以在 struct 內部盡情使用。在(2)(3)(4) 就分別用來當作

  • Array 儲存的資料型別
  • 函式的參數型別
  • 函式的 return type

定義完成後,若希望使用 Stack 這個 generic type 時,我們必須明確告訴 compiler 這個 Stack instance 會儲存何種型別,寫法如下:

var stack0 = Stack<String>()
// or
var stack1: Stack<String> = Stack()

當 generic type 被實例化,會稱該實例的泛型宣告(如本例的 <String>)為 Type Arguments,如同一般函式帶入引數的概念,原本虛擬的 type parameter 的 placeholder,將完全被 type argument 取代。此即 Generic Types 的用法,與 Generic Functions 如出一轍。

在不使用 literal 的狀況下,實例化 Generic Types 的語法與實例化 Array/Dictionary 一樣,畢竟 Array/Dictionary 也是內建的 generic types。

Type Constraints

有時候,演算法可能需要傳入的型別有實作特定方法,泛型可以透過設置 Type Constraints,確保傳入型別符合需求,簡單的範例如下:

func myFunction<T: SomeClass, U: SomeProtocol>(someT: T, someU: U) {
  // Here is function body
}

上例中有兩處新語法:

  • 宣告多個 type parameters 時,parameters 間使用 , 分隔。
  • 泛型型別名稱後,添加 : SomeClass: SomeProtocol,代表 T 的實際型別必須繼承 SomeClassU 的實際型別則需遵循 SomeProtocol

回過頭,看最初手機廠牌的範例,我們為每個廠牌加上一點特性。

protocol Chargeable {}          // 可充電的
protocol HeadphoneJack {}     // 有 Headphone Jack 的

extension AppleIPhone7: Chargeable {}
extension SamsungNote7: HeadphoneJack {}
extension AsusZenfone: Chargeable, HeadphoneJack {}

再定義三個 generic functions,但每個 functions 都有相對應的 type constraints,確保傳入的型別符合要求。

func charge<Phone: Chargeable>(phone: Phone) {
  print("\(type(of: phone)) is charging")
}

func listenMusic<Phone: HeadphoneJack>(phone: Phone) {
  print("Listen music with \(type(of: phone))")
}

func listenMusicWhileCharging<Phone: Chargeable & HeadphoneJack>(phone: Phone) {
  print("Listen music with \(type(of: phone)) while charging")
}

listenMusicWhileCharging(_:) 的 type constraint 為Chargeable & HeadphoneJack,為 protocol composition,是合法的 type constraint。

最後將手機實例化,並分別帶入所有 functions,

let iphone7 = AppleIPhone7()
let note7 = SamsungNote7()
let zenfone = AsusZenfone()

charge(phone: zenfone)
charge(phone: iphone7)
charge(phone: note7)

listenMusic(phone: zenfone)
listenMusic(phone: iphone7)
listenMusic(phone: note7)

listenMusicWhileCharging(phone: zenfone)
listenMusicWhileCharging(phone: iphone7)
listenMusicWhileCharging(phone: note7)

最後應該會編譯失敗,因為 AppleIPhone7SamsungNote7 分別沒有實作 HeadphoneJackchargeable,兩者的 instances 當然無法帶入不符合 type constraints 的 functions。

Generic Parameter Clause

到此,我們來看看泛型宣告的完整語法。

依照 Swift 官方說詞,泛型的宣告叫做 Generic Parameter Clause,是由角括號 <> 將 Generic Parameter 包裹起來,若有多個 parameters,則以 ,(comma-separated)分隔。

<GenericA, GenericB, GenericC>

而每個 Generic Parameters 除了虛擬 type name(官方:Type Parameter),還可有 optional 的 Type Constraints

最後,Generic Parameter Clause 語法大致會像這樣:

<GenericA, GenericB: SomeClass, GenericC: SomeProtocol>

Generic Parameter Clause 用在宣告型別,若對應泛型實例化,有 Generic Argument Clause,例如建構 Dictionary:let a = Dictionary<String, [Double]>()。將指定的實際型別帶入對應位置即可。

Associated Types

Generic Types 一節,我們提到 Swift 所有一等公民、typealias,以及 initializers 都可以利用 Generic Parameter Clauses 宣告泛型。那 protocols 呢?我們可以嘗試看看。

protocol Test<T> {}
// error: protocols do not allow generic parameters; use associated types instead

結果 Swift 不允許 protocol 使用我們熟悉的泛型宣告,而是引入另一個關鍵字 associatedtype。我們可以把 associatedtype,想像成 protocol 版的 generics,其有幾點特性:

  • 使用完整的繼承寫法,如 associatedtype MyType: MyClass, MyProtocol, AnotherProtocol
  • 可透過 Self access associated type
  • 承上,因此實際上是一個 nested type 的 protocol requirements。

有關為何捨棄 type parameterization,而特別設計 associatedtype 作為 protocol 的泛型,可參考這篇文章這個 Pokemon 範例,和這個 stackoverflow 回答

接下來,藉由幾個重要的 protocol 來理解 associatedtype。首先,我們來看 IteratorProtocolIteratorProtocol 是一切集合、容器、迴圈最基礎 protocol。擷取定義如下:

public protocol IteratorProtocol {

  associatedtype Element                        // 1

  public mutating func next() -> Self.Element?  // 2, 3
}
  1. 宣告一個泛型 associated type Element
  2. 必須實作 next() 方法,返回一個 OptionalElement
  3. 繼承 IteratorProtocol 型別,必須有 Element 這個 nested type,才能存取 Self.Element

再來,就可以實作一個無限迴圈的 ForeverIterator,並利用 Swift 的 Type Inference,自動判斷 Element 這個 associated type 實際的型別。

struct ForeverIterator: IteratorProtocol {
  func next() -> Bool? { // 從 declaration 推斷 Element 的實際型別
    return true
  }
}

我們也可以利用 typealias 顯式宣告 Element 的實際型別。讓該型別完成 nested type 的 requirement。

struct ForeverIterator: IteratorProtocol {
  typealias Element = Bool // 完成 nested type `Element` 的 requirement
  func next() -> Bool? {
    return true
  }
}

這樣就完成一個無限迴圈的 Iterator 了。

import Foundation

let it = ForeverIterator()

while let flag = it.next(), flag {
    Thread.sleep(forTimeInterval: 1)
    print(Date())
}

associatedtype 除了提供 protocol 相關的 generic type 之外,也可以直接提供 Default 的 type。簡單範例如下:

protocol GotDefaultAssociatedType {
  associatedtype SomeType = Double     // 提供 default type `Double`
}

struct SomeStruct: GotDefaultAssociatedType {   
  // 不需要宣告任何 typealias 或是實際的型別,就可以正確執行 `multiply(_:)`

  func multiply(x: SomeType) {
    print(x * x)
  }
}

SomeStruct().multiply(1.5)
// 2.25

看完上面的範例,可以了解 Associated Type 其實就是 protocol 版的 Generic Parameter Clause,彈性更大,可讀性更高,配合接下來的 Generic Where Clauses,更能展現 Associated Type 的強大。

Generic Where Clauses

在 Type Constraints 一節,提到使用 Type Constraints 限制傳入的型別,Swift 的泛型也有另外一個可讀性、彈性更高,更接近自然語言的 syntax —— Generic Where Clauses。以下兩種寫法等價:

func lessThan<T: Comparable>(x: T, y: T) -> Bool {
  return x < y
}
// is equivalent to
func lessThan<T>(x: T, y: T) -> Bool where T: Comparable  {
  return x < y
}

lessThan(123, 1234)

某些情境下,需確認「兩個泛型引數為相同型別」,才能進行操作,例如比較兩個 Sequence 是否完全一樣,可如下宣告:

// 在兩個 Sequence 擁有相同 Element 才能執行的 function
func someFunc<S1: Sequence, S2: Sequence>(s1: S1, s2: S2) where S1.Iterator.Element == S2.Iterator.Element, S1.Iterator.Element: Comparable {
  // skip the implementation
}

從上兩例中,我們可以理解 Generic Where Clause 相關特性如下:

  • 必定放在 declaration body {} 之前。
  • 接受 requirements,多個 requirements 以 , 分隔。

在前例中,我們也看到了 S1.Iterator.Element 這個奇怪的傢伙,這其實源自於 Sequenceassociatedtype

public protocol Sequence {

  associatedtype Iterator : IteratorProtocol

  public func makeIterator() -> Self.Iterator

  // other implementations ...
}

也是因為透過 associatedtype 的 nested type 特性,我們才能存取到 Sequence 的 Iterator 下的 Element associatedtype。再配合 Generic Where Clause,能夠更靈活運用 Swift 強大的 Generics。

我們知道 Swift for-loop 可以用 where clause 做進一步的判斷來 filter elements,而本節介紹的 Generic Where Clauses 則是針對泛型的宣告使用,別搞混了。

Some Thoughts of Generics

靜態定型(Static typing)與動態定型(Dynamic typing)是程式語言的兩大陣營。動態定型的語言強調彈性與可讀性,靜態定型的語言卻堅持宣告需清楚完整。而泛型程式設計的興起,讓靜態定型語言可以更靈活的設計與實作,卻讓宣告式變得又臭又長。

學習泛型語法要花不少精力,讓語言支援泛型更是一大工程。Go 語言的泛型至少從 2011 吵到現在,居然還沒個影子,大家還是趕快去學 Rust 吧XDD

Reference