Swift 4 中的泛型

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

譯者: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的遵循類型,而不需要再定義類型參數?,F在我們有所有商品的庫存列表。因為每個餐館都要從理事會中獲取授權去長時間存儲商品,我們依然沒有一個存儲的地方。所以,我們把我們的關注點放到理事會上。

關聯類型協議

我們再次回去到城市理事會去獲取存儲食物的允許。理事會規定了一些我們必須遵守的規則。例如,每家有倉庫的餐館都要自己清理自己的倉庫和把一些特定的食物彼此分開。同樣,理事會可以隨時檢查每間餐館的庫存。他們提供了每個倉庫都要遵循的協議。這個協議不能針對特定的餐館,因為倉庫物品可以改變成各種商品,并提供給餐館。在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協議?,F在看來,關聯類型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協議做了一些改進。當他們想檢查一切是否安好,他們想列出每一樣物品,并控制他們??刂七M程對于每個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地址

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容

  • 本章將會介紹 泛型所解決的問題泛型函數類型參數命名類型參數泛型類型擴展一個泛型類型類型約束關聯類型泛型 Where...
    寒橋閱讀 648評論 0 2
  • 原文:Generics Manifesto -- Douglas Gregor 譯者注 在我慢慢地深入使用 Swi...
    kemchenj閱讀 2,070評論 0 6
  • 泛型代碼可以確保你寫出靈活的,可重用的函數和定義出任何你所確定好的需求的類型。你的可以寫出避免重復的代碼,并且用一...
    iOS_Developer閱讀 812評論 0 0
  • 泛型(Generics) 泛型代碼允許你定義適用于任何類型的,符合你設置的要求的,靈活且可重用的 函數和類型。泛型...
    果啤閱讀 688評論 0 0
  • 136.泛型 泛型代碼讓你可以寫出靈活,可重用的函數和類型,它們可以使用任何類型,受你定義的需求的約束。你可以寫出...
    無灃閱讀 1,498評論 0 4