Swift 4 中的泛型

8,431 阅读11分钟

这是我基于英文原文翻译的译文,如果你对本文感兴趣而且想转发,你应该在转发文章里加上本文的链接

译者: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地址