Swift 4中的泛型–开发人员思想–中

以良好的格式阅读此文章,并在 Swift Post 上突出显示语法

作为Swift中最强大的功能之一,泛型可能会很棘手。 很多人在理解和使用它们时遇到麻烦,尤其是应用程序开发人员。 泛型最适合于库,框架和SDK。 在本文中,我将尝试与其他教程不同的方法。 我们将开设一家餐厅,并获得SwiftyCity市议会的许可。 为了诚实起见,我将尝试将事物归为四个主题。

  1. 泛型函数和泛型类型
  2. 关联类型的协议
  3. 通用条款
  4. 通用下标

开始了!

泛型函数和泛型类型

开一家斯威夫特餐厅

让我们建立一个新餐厅。 设置过程中,我们不仅会关注我们餐厅的建设,还将研究法律部分,并获得市议会的许可。 此外,我们将专注于我们自己的业务以使其运作并实现盈利。

首先,公司在理事会中的样子如何? 公司应具有一些基本功能。

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

buy功能将产品添加到库存中,并从公司的金库中取出资金。 另一方面, sell函数获取产品类型,创建/查找产品并返回以换取金钱。

泛型函数

在此协议中, 实际类型的 Product听起来不正确。 不可能将每个实际产品都放入Product类型。 每个产品具有不同的功能,特性等。 在这类函数中使用实际类型确实是个坏主意。 让我们回到理事会。 在世界各地,无论每个公司做什么,都需要具有购买和出售的功能。 因此,理事会必须找到针对这两个职能的通用解决方案,并使它们适用于每个公司。 他们可以使用Generics改进这些功能:

 protocol Company { 
  func buy(product: T, with money: Money) 
  func sell(product: T.Type, for money: Money) -> T? 
 } 

因此,我们将实际类型的 Product替换为占位符类型 T 类型参数 将这些功能定义为泛型。 在编译期间, 占位符类型将替换为实际类型 。 因此,将在使用买卖功能时确定实际类型 。 这提供了为每个产品使用相同功能的灵活性。 例如,我们将在Swift餐厅中出售Penne Arrabiata。 我们可以直接调用sell函数,如下所示:

 let penneArrabiata = swiftRestaurant.sell(product: PenneArrabiata.Self, for: Money(value:7.0, currency: .dollar)) 

在编译期间,编译器用PenneArrabiata的类型替换T 在运行时调用此方法时,它将已经具有实际类型 (PenneArrabiata)而不是占位符type 。 但是还有另一个问题。 我们不仅可以在这里买卖任何类型,还需要一些东西来定义我们可以合法买卖的东西。 这就是类型约束出现的地方。理事会还有另一个名为LegallyTradable协议。 它只是检查和标记我们可以合法买卖的产品。 安理会迫使我们将本议定书适用于所有贸易活动。 理事会将迭代每种产品,并采用本协议中的所有合适产品。 因此,我们必须对通用函数设置类型约束 ,以限制仅允许的产品使用函数。

 protocol Company { 
  func buy(product: T, with money: Money) 
  func sell(product: T.Type, 
for money: Money) -> T?
 } 

现在,我们可以内在地使用这些功能。 基本上,我们对占位符类型 T施加约束,说“只有符合LegallyTradable才能成为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 
  } 
 } 

一段时间后,我们的经理开始认为我们需要为餐厅中的每个商品创建一个列表,以更好地跟踪它们。 泛型可以在这里为我们提供帮助,而不是每次都创建单独的列表结构。 如果我们将清单清单定义为通用类型 ,则可以使用相同的struct实现轻松地创建新的清单清单。 与泛型函数相同, 类型参数 将我们的结构定义为generic 。 因此,我们只需要将PastaPackage 实际类型替换为占位符类型 T

 struct InventoryList { 
  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() 
 pastaInventory.add(item: PastaPackage()) 
 var tomatoSauceInventory = InventoryList() 
 var flourSackInventory = InventoryList() 

通用类型的另一个优点是,只要经理需要一些额外的信息(例如清单中的第一项),我们就可以编写扩展来添加功能。 Swift允许我们为结构,类和协议编写扩展 。 在扩展泛型类型时,我们不需要像定义结构时那样提供类型参数 。 但是,我们仍然可以在扩展中使用占位符类型。 让我们执行经理的要求以更好地理解。

 extension InventoryList { // No 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关键字,只要我们在协议中Item使用的所有位置都使用Food类型,它仍然可以编译。 Swift会自动处理。

用类型注释约束关联的类型

就像在现实生活中一样,理事会总是想出新的规则,迫使您遵守这些规则。 一段时间后,理事会修改了其存储协议。 他们宣布将不允许Storage所有物品。 每个Item 必须符合 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子句的泛型类型

让我们回到帖子的开头,看看我们Company协议中的Money类型。 正如我们都在谈论协议一样,买卖功能的货币参数实际上是一个协议。

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

不久之后,安理会再次回击我们,说他们决定再制定一条规则。 从现在开始,仅允许使用某些货币进行交易。 在此之前,我们可以使用Money类型的各种Money 。 他们没有使用每种货币定义新的货币类型,而是决定使用Money协议并按以下方式更改了买卖功能。

 protocol Company { 
  func buy(product: T.Type, with money: M) -> T? where M.Currency: TradeCurrency 
  func sell(product: T, for money: M) where M.Currency: TradeCurrency 
 } 

where子句类型约束之间的区别是where子句用于定义关联类型中的需求 。 换句话说,我们不限制协议内部的关联类型。 相反,我们在使用协议时限制了它。

使用通用Where子句进行扩展

where子句在扩展中的其他用法。 例如,当理事会需要以“ xxx EUR”之类的好格式打印货币时,他们可以在Money上添加扩展名,将其币种限制为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 
 } 

迭代器协议具有关联的类型,称为Element ,在这里,我们添加了一个要求,说Element的类型必须等于Storage协议中的Item的类型。

通用下标

经理和理事会的要求似乎无止境。 另外,我们在这里实现他们的愿望。 因此,我们的经理来找我们的时候说她想使用Sequence来访问存储项目而不访问所有项目。 基本上,经理想要语法糖。

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

在Swift 4中,下标可以是通用的,我们可以使用通用的where子句。 在我们的用法中,索引参数必须符合Sequence协议。 从Apple文档中可以看出,“泛型where子句要求序列的迭代器必须遍历Int类型的元素。”这可确保序列中的索引与存储中使用的索引类型相同。

最后的话

我们使我们的餐厅功能齐全。 我们的经理和理事会似乎很高兴。 从我们的旅程中可以看到,泛型确实很强大。 一旦获得概念,我们便可以使用泛型轻松满足广泛的需求。 泛型在Swift标准库中被广泛使用。 例如,数组和字典类型都是通用集合。 如果您想学习更多并深入学习,可以随时查看这些类及其实现方式。 Swift Language Doc还提供了泛型的广泛解释。 最后,Swift语言有Generic Manifesto,它解释了泛型的功能和未来的方向。 建议您阅读所有文档,以了解当前的用法和未来的计划。