Swift 4 中的泛型

britzlieg發表於2017-10-13

這是我基於英文原文翻譯的譯文,如果你對本文感興趣而且想轉發,你應該在轉發文章里加上本文的連結

譯者:britzlieg

英文原文連結

作為Swift中最重要的特性之一,泛型使用起來很巧妙。很多人都不太能理解並使用泛型,特別是應用開發者。泛型最適合libraries, frameworks, and SDKs的開發。在這篇文章中,我將用不同於其他教程的角度來講解泛型。我們將使用餐館的例子,這個餐館能從SwiftCity的城市理事會中獲得授權。為了保持簡潔,我將內容控制在以下四個主題:

  • 1、泛型函式和泛型型別
  • 2、關聯型別協議
  • 3、泛型的Where語句
  • 4、泛型下標

我們接下來看看具體怎麼做!

泛型函式和泛型型別

開一家Swift餐館

讓我們新開張一家餐館。當開張的時候,我們不僅關注餐館的結構,也關注來自城市理事會的授權。更重要的,我們將關注我們的業務,以便於它功能化和有利可圖。首先,怎麼讓一家公司怎麼看上去像一個理事會?一個公司應該要有一些基礎的功能。


protocol Company {
  func buy(product: Product, money: Money)
  func sell(product: Product.Type, money: Money) -> Product?
}複製程式碼

buy函式把商品新增到庫存中,並花費公司相應的現金。sell函式建立/查詢所需花費的該型別商品,並返回出售的商品。

泛型函式

在這個協議中,Product如果是一個確定的型別的話不太好。把每一個product統一成一個確定的商品型別是不可能的。每個商品都有自己的功能,屬性等。在這些各種型別的函式中,使用一個確定的型別是一個壞主意。讓我們回到理事會那裡看看。總而言之,不管是哪個公司,它都需要購買和賣出商品。所以,理事會必須找到適合這兩個功能的一種通用的解決方案,以適合於每家公司。他們可以使用泛型來解決這個問題。

protocol Company {
  func buy<T>(product: T, with money: Money)
  func sell<T>(product: T.Type, for money: Money) -> T?
}複製程式碼

我們把我們原來的確定型別Product用預設型別T來代替。這個型別引數<T>把這些函式定義成泛型。在編譯時,預設型別會被確定型別替代。當buy和sell函式被呼叫時,具體型別就會被確定下來。這使得不同產品能靈活使用同一個函式。例如,我們在Swift餐館中賣Penne Arrabiata。我們可以像下面一樣直接呼叫sell函式:

let penneArrabiata = swiftRestaurant.sell(product: PenneArrabiata.Self, for: Money(value:7.0, currency: .dollar))複製程式碼

在編譯時,編譯器用型別PenneArrabiata替換型別T。當這個方法在執行時被呼叫的時候,它已經時有一個確定的型別PenneArrabiata而不是一個預設的型別。但這帶來另外一個問題,我們不能只是簡單的買賣各種型別的商品,還要定義哪些商品時能夠被合法買賣。這裡就引入where型別約束。理事會有另一個協議LegallyTradable。它將檢查和標記我們可以合法買賣的商品。理事會強制我們對所有買賣實行這個協議,並列舉每一個符合協議的從商品。所以我們需要為我們的泛型函式新增約束,以限制只能買賣符合協議的商品。

protocol Company {
  func buy<T: LegallyTradable>(product: T, with money: Money)
  func sell<T: LegallyTradable>(product: T.Type, for money: Money) -> T?
}複製程式碼

現在,我們可以放心用這些函式了。通常,我們把符合LegallyTradable協議的預設型別T作為我們Company協議函式的引數。這個約束被叫做Swift中的協議約束。如果一個商品不遵循這個協議,它將不能作為這個函式的引數。

泛型型別

我們把注意力轉移到我們的餐館上。我們得到授權並準備關注餐館的管理。我們聘請了一位出色的經理和她想建立一套能跟蹤商品庫存的系統。在我們的餐館中,我們有一個麵食選單,顧客喜歡各種各樣的麵食。這就是我們為什麼需要一個很大的地方去儲存麵食。我們建立一個麵食套餐列表,當顧客點套餐的時候,將套餐從列表中移除。無論何時,餐館會買麵食套餐,並把它加到我們的列表中。最後,如果列表中的套餐少於三個,我們的經理將訂新的套餐。這是我們的PastaPackageList結構:

struct PastaPackageList {
  var packages: [PastaPackage]

  mutating func add(package: PastaPackage) {
    packages.append(item)
  }

  mutating func remove() -> PastaPackage {
    return packages.removeLast()
  }

  func isCapacityLow() -> Bool {
    return packages.count < 3
  }
}複製程式碼

過了一會,我們的經理開始考慮為餐館中的每一樣商品建立一個列表,以便更好的跟蹤。與其每次建立獨立列表結構,不如用泛型來避免這個問題。如果我們定義我們的庫存列表作為一個泛型類,我們可以很容易使用同樣的結構實現建立新的庫存列表。與泛型函式一樣,使用引數型別<T>定義我們的結構。所以我們需要用T預設型別來替代PastaPackage具體型別

struct InventoryList<T> {
  var items: [T]

  mutating func add(item: T) {
    items.append(item)
  }

  mutating func remove() -> T {
    return items.removeLast()
  }

  func isCapacityLow() -> Bool {
    return items.count < 3
  }
}複製程式碼

這些泛型型別讓我們可以為每個商品建立不同的庫存列表,而且使用一樣的實現。

var pastaInventory = InventoryList<PastaPackage>()
pastaInventory.add(item: PastaPackage())
var tomatoSauceInventory = InventoryList<TomatoSauce>()
var flourSackInventory = InventoryList<FlourSack>()複製程式碼

泛型的另外一個優勢是隻要我們的經理需要額外的資訊,例如庫存中的第一種商品,我們都可以通過使用擴充套件來新增功能。Swift允許我們去寫結構體,類和協議的擴充套件。因為泛型的擴充套件性,當我們定義結構體時,不需要提供型別引數。在擴充套件中,我們仍然用預設型別。讓我們看看我們如何實現我們經理的需求。

extension InventoryList { // We define it without any type parameters
  var topItem: T? {
    return items.last
  }
}複製程式碼

InventoryList中存在型別引數T作為型別topItem的遵循型別,而不需要再定義型別引數。現在我們有所有商品的庫存列表。因為每個餐館都要從理事會中獲取授權去長時間儲存商品,我們依然沒有一個儲存的地方。所以,我們把我們的關注點放到理事會上。

關聯型別協議

我們再次回去到城市理事會去獲取儲存食物的允許。理事會規定了一些我們必須遵守的規則。例如,每家有倉庫的餐館都要自己清理自己的倉庫和把一些特定的食物彼此分開。同樣,理事會可以隨時檢查每間餐館的庫存。他們提供了每個倉庫都要遵循的協議。這個協議不能針對特定的餐館,因為倉庫物品可以改變成各種商品,並提供給餐館。在Swift中,泛型協議一般用關聯型別。讓我們看看理事會的倉庫協議是怎麼樣的。

protocol Storage {
  associatedtype Item
  var items: [Item] { set get }
  mutating func add(item: Item)
  var size: Int { get }
  mutating func remove() -> Item
  func showCurrentInventory() -> [Item]
}複製程式碼

Storage協議並沒有規定物品怎麼儲存和什麼型別被允許儲存。在所有商店,實現了Storage協議的餐館必須制定一種他們他們儲存的特定型別的商品。這要保證物品從倉庫中新增和移除的正確性。同樣的,它必須能夠完整展示當前倉庫。所以,對於我們的倉庫,我們的Storage協議如下所示:

struct SwiftRestaurantStorage: Storage {
  typealias Item = Food // Optional
  var items = [Food]()
  var size: Int { return 100 }
  mutating func add(item: Food) { ... }
  mutating func remove() -> Food { ... }
  func showCurrentInventory() -> [Food] { ... }
}複製程式碼

我們實現理事會的Storage協議。現在看來,關聯型別Item可以用我們的Food型別來替換。我們的餐館倉庫都可以儲存Food。關聯型別Item只是一個協議的預設型別。我們用typealias關鍵字來定義型別。但是,需要指出的是,這個關鍵字在Swift中是可選的。即使我們不用typealias關鍵字,我們依然可以用Food替換協議中所有用到Item的地方。Swift會自動處理這個。

限制關聯型別為特定型別

事實上,理事會總是會想出一些新的規則並強制你去遵守。一會後,理事會改變了Storage協議。他們宣佈他們將不允許任何物品在Storage。所有物品必須遵循StorableItem協議,以保證他們都適合儲存。換句話,它們都限制為關聯型別Item

protocol Storage {
  associatedtype Item: StorableItem // Constrained associated type
  var items: [Item] { set get }
  var size: Int { get }
  mutating func add(item: Item)
  mutating func remove() -> Item
  func showCurrentInventory() -> [Item]
}複製程式碼

用這個方法,理事會限制型別為當前關聯型別。任何實現Storage協議的都必須使用實現StorableItem協議的型別。

泛型的Where語句

使用泛型的Where語句的泛型

讓我們回到文章剛開始的時候,看看Company協議中的Money型別。當我們討論到協議時,買賣中的money引數事實上是一個協議。

protocol Money {
  associatedtype Currency
  var currency: Currency { get }
  var amount: Float { get }
  func sum<M: Money>(with money: M) -> M where M.Currency == Currency
}複製程式碼

然後,再過了一會,理事會打回了這個協議,因為他們有另一個規則。從現在開始,交易只能用一些特定的貨幣。在這個之前,我們能各種用Money型別的貨幣。不同於每種貨幣定義money型別的做法,他們決定用Money協議來改變他們的買賣函式。

protocol Company {
  func buy<T: LegallyTradable, M: Money>(product: T.Type, with money: M) -> T? where M.Currency: TradeCurrency
  func sell<T: LegallyTradable, M: Money>(product: T, for money: M) where M.Currency: TradeCurrency
}複製程式碼

where語句和型別約束的where語句的區別在於,where語句會被用於定義關聯型別。換句話,在協議中,我們不能限制關聯的型別,而會在使用協議的時候限制它。

泛型的where語句的擴充套件

泛型的where語句在擴充套件中有其他用法。例如,當理事會要求用漂亮的格式(例如“xxx EUR”)列印money時,他們只需要新增一個Money的擴充套件,並把Currency限制設定成`Euro

extension Money where Currency == Euro {
  func printAmount() {
    print("\(amount) EUR")
  }
}複製程式碼

泛型的where語句允許我們新增一個新的必要條件到Money擴充套件中,因此只有當CurrencyEuro時,擴充套件才會新增printAmount()方法。

泛型的where 語句的關聯型別

在上文中,理事會給Storage協議做了一些改進。當他們想檢查一切是否安好,他們想列出每一樣物品,並控制他們。控制程式對於每個Item是不一樣的。因為這樣,理事會僅僅需要提供Iterator關聯型別到Storage協議中。

protocol Storage {
  associatedtype Item: StorableItem
  var items: [Item] { set get }
  var size: Int { get }
  mutating func add(item: Item)
  mutating func remove() -> Item
  func showCurrentInventory() -> [Item]

  associatedtype Iterator: IteratorProtocol where Iterator.Element == Item
  func makeIterator() -> Iterator
}複製程式碼

Iterator協議有一個叫Element``的關聯型別。在這裡,我們給它加上一個必要條件,在Storage協議中,Element必須與Item```型別相等。

泛型下標

來自經理和理事會的需求看起來是無窮無盡的。同樣的,我們需要滿足他們的要求。我們的經理跑過來跟我們說她想要用一個Sequence來訪問儲存的物品,而不需要訪問所有的物品。經理想要個語法糖。

extension Storage {
  subscript<Indices: Sequence>(indices: Indices) -> [Item] where Indices.Iterator.Element == Int {
    var result = [Item]()
    for index in indices {
      result.append(self.items[index])
    }
    return result
  }
}複製程式碼

在Swift 4中,下標也可以是泛型,我們可以用條件泛型來實現。在我們的使用中,indices引數必須實現Sequence協議。從Apple doc中可以知道,“The generic where clause requires that the iterator for the sequence must traverse over elements of type Int.”這就保證了在sequence的indices跟儲存中的indices是一致的。

結語

我們讓我們的餐館功能完備。我們的經理和理事會看起來也很高興。正如我們在文章中看到的,泛型是很強大的。我們可以用泛型來滿足各種敏感的需求,只要我們知道概念。泛型在Swift的標準庫中也應用廣泛。例如,ArrayDictionary型別都是泛型集合。如果你想知道更多,你可以看看這些類是怎麼實現的。 Swift Language Doc 也提供了泛型的解析。最近Swift語言提供了泛型的一些說明Generic Manifesto。我建議你去看完所有的文件,以便更好的理解當前用法和未來的規劃。感謝大家的閱讀!如果你對接下來的文章有疑惑,建議,評論或者是想法,清在 Twitter 聯絡我,或者評論!你也可以在GitHub上關注我哦!

本文Github地址

相關文章