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的遵循类型,而不需要再定义类型参数。现在我们有所有商品的库存列表。因为每个餐馆都要从理事会中获取授权去长时间存储商品,我们依然没有一个存储的地方。所以,我们把我们的关注点放到理事会上。

关联类型协议

我们再次回去到城市理事会去获取存储食物的允许。理事会规定了一些我们必须遵守的规则。例如,每家有仓库的餐馆都要自己清理自己的仓库和把一些特定的食物彼此分开。同样,理事会可以随时检查每间餐馆的库存。他们提供了每个仓库都要遵循的协议。这个协议不能针对特定的餐馆,因为仓库物品可以改变成各种商品,并提供给餐馆。在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地址

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,839评论 6 482
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,543评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 153,116评论 0 344
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,371评论 1 279
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,384评论 5 374
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,111评论 1 285
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,416评论 3 400
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,053评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,558评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,007评论 2 325
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,117评论 1 334
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,756评论 4 324
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,324评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,315评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,539评论 1 262
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,578评论 2 355
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,877评论 2 345

推荐阅读更多精彩内容

  • 本章将会介绍 泛型所解决的问题泛型函数类型参数命名类型参数泛型类型扩展一个泛型类型类型约束关联类型泛型 Where...
    寒桥阅读 631评论 0 2
  • 原文:Generics Manifesto -- Douglas Gregor 译者注 在我慢慢地深入使用 Swi...
    kemchenj阅读 1,917评论 0 6
  • 泛型代码可以确保你写出灵活的,可重用的函数和定义出任何你所确定好的需求的类型。你的可以写出避免重复的代码,并且用一...
    iOS_Developer阅读 790评论 0 0
  • 泛型(Generics) 泛型代码允许你定义适用于任何类型的,符合你设置的要求的,灵活且可重用的 函数和类型。泛型...
    果啤阅读 665评论 0 0
  • 136.泛型 泛型代码让你可以写出灵活,可重用的函数和类型,它们可以使用任何类型,受你定义的需求的约束。你可以写出...
    无沣阅读 1,452评论 0 4