Swift 4 中的泛型

这是我基于英文原文翻译的译文,若是你对本文感兴趣并且想转发,你应该在转发文章里加上本文的连接html

译者:britzlieggit

英文原文连接github

做为Swift中最重要的特性之一,泛型使用起来很巧妙。不少人都不太能理解并使用泛型,特别是应用开发者。泛型最适合libraries, frameworks, and SDKs的开发。在这篇文章中,我将用不一样于其余教程的角度来说解泛型。咱们将使用餐馆的例子,这个餐馆能从SwiftCity的城市理事会中得到受权。为了保持简洁,我将内容控制在如下四个主题:swift

  • 一、泛型函数和泛型类型
  • 二、关联类型协议
  • 三、泛型的Where语句
  • 四、泛型下标

咱们接下来看看具体怎么作!app

泛型函数和泛型类型

开一家Swift餐馆

让咱们新开张一家餐馆。当开张的时候,咱们不只关注餐馆的结构,也关注来自城市理事会的受权。更重要的,咱们将关注咱们的业务,以便于它功能化和有利可图。首先,怎么让一家公司怎么看上去像一个理事会?一个公司应该要有一些基础的功能。函数

protocol Company {
  func buy(product: Product, money: Money)
  func sell(product: Product.Type, money: Money) -> Product?
}复制代码

buy函数把商品添加到库存中,并花费公司相应的现金。sell函数建立/查找所需花费的该类型商品,并返回出售的商品。post

泛型函数

在这个协议中,Product若是是一个肯定的类型的话不太好。把每个product统一成一个肯定的商品类型是不可能的。每一个商品都有本身的功能,属性等。在这些各类类型的函数中,使用一个肯定的类型是一个坏主意。让咱们回到理事会那里看看。总而言之,不论是哪一个公司,它都须要购买和卖出商品。因此,理事会必须找到适合这两个功能的一种通用的解决方案,以适合于每家公司。他们可使用泛型来解决这个问题。ui

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函数:spa

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地址

相关文章
相关标签/搜索