Swift & Cocoa 實戰之泛型編程:Swift 中的泛型編程

Generics in Swift編程

Swift 中的泛型編程

本文中,筆者旨在對於Swift中的泛型編程進行一個綜合性描述。讀者能夠查看上一篇 系列中的描述來看以前筆者的論文。泛型編程是編程方式的一種,主要是一些譬如類、結構體以及枚舉這樣的複雜類型以及函數可使用類型參數進行定義(type parameters)。類型參數能夠看作是真實類型的佔位符,當泛型或者函數被使用時纔會被真實類型替換。swift

在Swift中,對於泛型編程最直觀的表現當屬Array這個類型。在Objective-C中,Array的示例NSArray能夠包含任意類型的對象。可是,Swift中的Array類型須要在聲明時即指定它們包含的元素類型,,譬如Array<Int>, Array<UIView>。Array與Int都是一種數據類型,泛型機制容許這兩種類型有機組合起來從而可以傳遞更多的額外的信息。數組

備註:在Swift中慣用的聲明Array包含了Foo類型的用法是[Foo]這種語法,而本文中使用Array<Foo>寫法旨在幫助理解以及強調Array是一個通用泛型。安全

Why generics (爲何使用泛型)?

這裏列舉出幾個靜態類型語言中使用泛型編程的緣由:app

  • 類型安全:相似於Array這樣的容器類型,能夠給出它們存放的具體的元素的類型,從而告知編譯器能夠插入容器中的對象的類型以及從容器中返回的對象的類型。這種機制做用於全部能夠被當作Array參數的類型,而且也做用於類型之間的關係。函數

  • 減小冗餘代碼:有些狀況下須要對多個數據類型進行相同的操做,能夠用一個泛型方程來代替多個不一樣類型參數或者返回值的重複的方程。這樣能夠避免潛在的代碼錯誤等等。ui

  • 靈活的依賴庫:第三方庫能夠暴露一些接口,從而避免使用這些接口的開發者被強制使用或者類型或者返回值爲一個固定的類型。它們可使用泛型進行更加抽象地編程,舉例來講,泛型會容許一個接口來接受不只僅是一個Array參數,而是一個Collection參數。this

Generic entities (泛型實體)

Swift中的泛型編程主要表如今如下兩個不一樣的狀況下:當定義某個類型或者定義某個函數時。泛型類型的特性以及<>這兩個關鍵字每每意味着某個類型或者函數是泛型。code

Generic types (泛型類型)

Swift中主要的三個用戶可自定義的類型能夠被當作泛型,下面就以Result枚舉類型爲例,該類型中存放了表徵成功的Success以及表徵失敗的Failure:orm

enum Result<T, U> {
  case Success(T)
  case Failure(U)
}

在Result類型以後有兩個類型參數: T以及U。這些類型參數將會在建立實例時被替換爲真實的類型:

let aSuccess : Result<Int, String> = .Success(123)
let aFailure : Result<Int, String> = .Failure("temperature too high")

泛型類型的類型參數能夠被用於如下的幾個方面:

  • 做爲屬性的類型

  • 做爲枚舉體中的關聯類型

  • 做爲某個方法的返回值或者參數

  • 做爲構造器的參數類型

  • 做爲另外一個泛型的類型參數,譬如Array

泛型能夠在如下兩種方式中被初始化:

  • 在建立新的實例時,類型參數被用戶給出的真實的數據類型替換。

  • 類型被推導得出,經過調用初始化器或者靜態方法來建立某個實例。

struct Queue<T> { /* ... */ }

// First way to instantiate
let a1 : Queue<Int> = Queue()
let a2 : Queue<Int> = Queue.staticFactoryMethod() // or just .staticFactoryMethod()

// Second way to instantiate
let b1 = Queue<Int>()
let b2 = Queue<Int>.staticFactoryMethod()

注意,像T這樣的類型參數在類型定義時出現,不管這個類型什麼時候被調用,這些類型參數都會被替換爲真實的類型。舉例而言,Array<String>或者Array<UIWindow>,即是以固定類型初始化的數組。而若是是像Array<T>這樣的,它便一直處於由其餘類型或者函數做爲類型參數定義的上下文中。

Generic functions (泛型函數)

函數、方法、屬性、下標以及初始化器均可以當作泛型進行處理,它們自身能夠成爲泛型或者存在於某個泛型類型的上下文中:

// Given an item, return an array with that item repeated n times
func duplicate<T>(item: T, numberOfTimes n: Int) -> [T] {
  var buffer : [T] = []
  for _ in 0 ..< n {
    buffer.append(item)
  }
  return buffer
}

一樣的,類型參數是定義在<>緊跟在函數名以後。它主要能夠用於如下幾個方面:

  • 做爲某個函數的參數

  • 做爲函數的返回值

  • 做爲另外一個泛型類型的類型參數,譬如T能夠做爲Array<T?>中的一部分

固然,若是全部的類型參數都是未用狀態編譯器會不太友好。而泛型方法能夠同時定義在泛型類型與非泛型類型上:

extension Result {
  // Transform the value of the 'Result' using one of two mapping functions
  func transform<V>(left: T -> V, right: U -> V) -> V {
    switch self {
    case .Success(let value): return left(value)
    case .Failure(let value): return right(value)
    }
  }
}

上述中的transform方法就是泛型方法,存放於泛型類型Result中。除了由Result中定義的類型參數T、U,泛型方法自己也定義了一個泛型參數V。當調用一個泛型方程時,不必定須要指明清楚傳入的類型參數。編譯器的類型推導接口會自動根據參數與返回類型推導出相關信息:

// Example use of the 'duplicate:numberOfTimes:' function defined earlier.
// T is inferred to be Int.
let a = duplicate(52, numberOfTimes: 10)

實際上,嘗試去清楚地設置參數類型還會引起錯誤:

// Does not compile
// Error: "Cannot explicitly specialize a generic function"
let b = duplicate<String>("foo", numberOfTimes: 5)

Associated types (關聯類型)

Swift中的Protocol不可使用類型參數定義泛型,不過Protocol可使用typealias關鍵字來定義一些關聯類型:

// A protocol for things that can accept food.
protocol FoodEatingType {
  typealias Food

  var isSatiated : Bool { get }
  func feed(food: Food)
}

在這個實例中,Food是定義在FoodEatingType協議中的關聯類型,而某個協議中的關聯類型數目能夠根據實際的需求數量定義。

關聯類型,相似於類型參數,都是一種佔位符。然後面若是某個類須要實現這個協議,則須要肯定Food的具體類型,是Hay仍是Rabbit。具體的作法就是繼承而且實現協議的屬性與方法,並在實現時再判斷應該用哪些真實的數據類型去替換這些關聯類型:

class Koala : FoodEatingType {
  var foodLevel = 0
  var isSatiated : Bool { return foodLevel < 10 }

  // Koalas are notoriously picky eaters
  func feed(food: Eucalyptus) {
    // ...
    if !isSatiated {
      foodLevel += 1
    }
  }
}

對於Koala這個具體的實現者,關聯類型Food被定義爲了Eucalyptus,換言之,也就是Koala.Food被定義爲了Eucalyptus。若是某個類型實現了多個協議,那麼對於每一個協議中的關聯類型也都必須實現。而若是某個繼承的類型也是個泛型,也可使用類型參數去幫助肯定關聯類型:

// Gourmand Wolf is a picky eater and will only eat his or her favorite food.
// Individual wolves may prefer different foods, though.
class GourmandWolf<FoodType> : FoodEatingType {
  var isSatiated : Bool { return false }

  func feed(food: FoodType) {
    // ...
  }
}

let meela = GourmandWolf<Rabbit>()
let rabbit = Rabbit()
meela.feed(rabbit)

在上述代碼中, GourmandWolf<Goose>.FoodGoose, 而 GourmandWolf<Sheep>.FoodSheep。順便說一句,協議中的關聯類型雖然屬於泛型,但也能夠爲其添加一些約束或者父類。譬如咱們定義的某個協議中爲Heap添加了一些列的操做,而且要保證全部的Heap的Key是可比較的,即必須是Comparable的子類或者實現,那麼能夠添加以下約束:

// Types that conform represent simple max-heaps which use their elements as keys
protocol MaxHeapType {
  // Elements must support comparison ops; e.g. 'a is greater than b'.
  typealias Element : Comparable

  func insert(x: Element)
  func findMax() -> Element?
  func deleteMax() -> Bool
}

Type constraints (類型約束)

截至目前,咱們說起的泛型,諸如T以及U能夠用任意類型來替換。而標準庫中的Array類型便是這樣一種無任何約束的典型,能夠由其類型參數來決斷。譬以下面一個例子中,須要編寫一個函數,輸入一個數組而獲取數組中最大的那個值而且返回:

// Doesn't compile.
func findLargestInArray<T>(array: Array<T>) -> T? {
  if array.isEmpty { return nil }
  var soFar : T = array[0]
  for i in 1..<array.count {
    soFar = array[i] > soFar ? array[i] : soFar
  }
  return soFar
}

不過這樣毫無限制的泛型對於編譯器是很是不友好的,如上述代碼中須要進行一個比較,即array[i] > soFar,而咱們只知道array[i]是類型T,而且soFar也是類型T。可是編譯器根本不知道這個類型T可否進行比較。譬如咱們建立一個空的結構體Foo,而且把它當作類型參數傳了進來,那麼咱們壓根不知道>這個比較運算符可否起做用。

在上文對於Protocol的討論中,咱們已經嘗試使用:爲某個關聯類型設置一些約束,而在剛纔的例子中,若是傳入的String類型是能夠正常編譯的,可是一旦傳入的是NSView類型,則不能正常編譯了。而咱們能夠以以下方式添加約束:

// Note that <T> became <T : Comparable>, meaning that whatever type fills in
// 'T' must conform to Comparable.
func findLargestInArray<T : Comparable>(array: Array<T>) -> T? {
  if array.isEmpty { return nil }
  var soFar : T = array[0]
  for i in 1..<array.count {
    soFar = array[i] > soFar ? array[i] : soFar
  }
  return soFar
}

// Example usage:
let arrayToTest = [1, 2, 3, 100, 12]
// We're calling 'findLargestInArray()' with T = Int.
if let result = findLargestInArray(arrayToTest) {
  print("the largest element in the array is \(result)")
} else {
  print("the array was empty...")
}
// prints: "the largest element in the array is 100"

這是由於,在某種意義上,存在着一種悖論,越限制類型參數與添加約束,用戶越能方便地使用這些參數。不加任何限制地類型參數每每只能使用在簡單地交換或者從集合中添加或者刪除某些元素。

簡單約束

func example<T, U : Equatable, V : Hashable>(foo: T, bar: U, baz: V) {
  // ...
}

若是須要對泛型進行細粒度的控制,首先來討論下在哪些地方能夠進行控制處理:

  • 相似於U,V這樣的類型參數,即上文中的普通的泛型參數

  • 類型參數的關聯類型,即上文中協議裏的關聯類型。

Swift中提供瞭如下三個類型的約束:

  • T : SomeProtocol:類型T必須聽從協議SomeProtocol。須要注意,使用protocol<Foo,Bar>

  • T==U:類型參數T必須是類型參數或者關聯類型U。

  • T:SomeClass:T必須是一個類,更加具體而言,T必須是SomeClass的一個實例或者它的子類。

Putting it all together

上文中已經介紹了基本的泛型的用法和約束,而對於泛型類型的簽名能夠綜合使用以下:

  • 聲明類型參數,若是願意的話,最好每一個類型參數都要聲明遵循某個協議。

  • 使用where關鍵字。

  • 使用逗號分割聲明約束。

protocol Foo {
  typealias Key
  typealias Element
}

protocol Bar {
  typealias RawGeneratorType
}

func example<T : Foo, U, V where V : Foo, V : Bar, T.Key == V.RawGeneratorType, U == V.Element>
  (arg1: T, arg2: U, arg3: V) -> U {
  // ...
}

不要驚慌,咱們會一步一步地介紹這些泛型的用法。在where關鍵字以前,咱們聲明瞭三個類型參數:T,U以及V。其中T必須遵循Foo協議。而在where關鍵字以後,咱們聲明瞭四個約束:

  • V必須遵循Foo協議。

  • V也必須實現了Bar協議。

  • 因爲T實現了Foo協議,T有一個關聯類型爲T.Key。V還有另外一個關聯類型,V.RawGeneratorType。這兩個類型必須是相同的:T.Key == V.RawGeneratorType。

  • 由於V實現了協議Foo,V包含一個關聯類型V.Element。這個類型必須與U一致,U == V.Element。

綜上所述,不管什麼時候使用example()函數時,必需要選擇合適的T、U以及V類型。

Constrained extensions

Swift 2中新近提出了約束擴展的概念,一個更強大的可以使用泛型的語法特性。在Swift中,擴展容許向任意類型,即便還沒有定義的類型中添加方法。一樣容許向某個協議中添加默認的實現方法。一樣的,這樣的基於約束的擴展能夠方便某些泛型的用法:

  • 對於像Array這樣的泛型,能夠在類型參數符合某個特定的約束的狀況下添加某個方法。

  • 對於像CollectionType這樣包含關聯類型的協議,能夠當某個關聯類型符合某個約束時添加默認的實現方法。

Syntax and limitations (語法與限制)

基於約束的擴展語法以下所示:

// Methods in this extension are only available to Arrays whose elements are
// both hashable and comparable.
extension Array where Element : Hashable, Element : Comparable {
  // ...
}

where關鍵字跟隨在類型參數以後,然後跟着以逗號分割的一系列泛型類型和參數。不過這其中的約束中並不能限定爲非泛型,即:

// An extension on Array<Int>.
// Error: "Same-type requirement makes generic parameter 'Element' non-generic"
extension Array where Element == Int {
  // ...
}

同時,也不能進行協議的傳遞:

protocol MyProtocol { /* ... */ }

// Only Arrays with comparable elements conform to MyProtocol.
// Error: "Extension of type 'Array' with constraints cannot have an inheritance clause"
extension Array : MyProtocol where Element : Comparable {
  // ...
}
相關文章
相關標籤/搜索