UIKit框架(四十一) —— 使用协议构建自定义Collection(一)

版本记录

版本号 时间
V1.0 2020.06.27 星期六

前言

iOS中有关视图控件用户能看到的都在UIKit框架里面,用户交互也是通过UIKit进行的。感兴趣的参考上面几篇文章。
1. UIKit框架(一) —— UIKit动力学和移动效果(一)
2. UIKit框架(二) —— UIKit动力学和移动效果(二)
3. UIKit框架(三) —— UICollectionViewCell的扩张效果的实现(一)
4. UIKit框架(四) —— UICollectionViewCell的扩张效果的实现(二)
5. UIKit框架(五) —— 自定义控件:可重复使用的滑块(一)
6. UIKit框架(六) —— 自定义控件:可重复使用的滑块(二)
7. UIKit框架(七) —— 动态尺寸UITableViewCell的实现(一)
8. UIKit框架(八) —— 动态尺寸UITableViewCell的实现(二)
9. UIKit框架(九) —— UICollectionView的数据异步预加载(一)
10. UIKit框架(十) —— UICollectionView的数据异步预加载(二)
11. UIKit框架(十一) —— UICollectionView的重用、选择和重排序(一)
12. UIKit框架(十二) —— UICollectionView的重用、选择和重排序(二)
13. UIKit框架(十三) —— 如何创建自己的侧滑式面板导航(一)
14. UIKit框架(十四) —— 如何创建自己的侧滑式面板导航(二)
15. UIKit框架(十五) —— 基于自定义UICollectionViewLayout布局的简单示例(一)
16. UIKit框架(十六) —— 基于自定义UICollectionViewLayout布局的简单示例(二)
17. UIKit框架(十七) —— 基于自定义UICollectionViewLayout布局的简单示例(三)
18. UIKit框架(十八) —— 基于CALayer属性的一种3D边栏动画的实现(一)
19. UIKit框架(十九) —— 基于CALayer属性的一种3D边栏动画的实现(二)
20. UIKit框架(二十) —— 基于UILabel跑马灯类似效果的实现(一)
21. UIKit框架(二十一) —— UIStackView的使用(一)
22. UIKit框架(二十二) —— 基于UIPresentationController的自定义viewController的转场和展示(一)
23. UIKit框架(二十三) —— 基于UIPresentationController的自定义viewController的转场和展示(二)
24. UIKit框架(二十四) —— 基于UICollectionViews和Drag-Drop在两个APP间的使用示例 (一)
25. UIKit框架(二十五) —— 基于UICollectionViews和Drag-Drop在两个APP间的使用示例 (二)
26. UIKit框架(二十六) —— UICollectionView的自定义布局 (一)
27. UIKit框架(二十七) —— UICollectionView的自定义布局 (二)
28. UIKit框架(二十八) —— 一个UISplitViewController的简单实用示例 (一)
29. UIKit框架(二十九) —— 一个UISplitViewController的简单实用示例 (二)
30. UIKit框架(三十) —— 基于UICollectionViewCompositionalLayout API的UICollectionViews布局的简单示例(一)
31. UIKit框架(三十一) —— 基于UICollectionViewCompositionalLayout API的UICollectionViews布局的简单示例(二)
32. UIKit框架(三十二) —— 替换Peek and Pop交互的基于iOS13的Context Menus(一)
33. UIKit框架(三十三) —— 替换Peek and Pop交互的基于iOS13的Context Menus(二)
34. UIKit框架(三十四) —— Accessibility的使用(一)
35. UIKit框架(三十五) —— Accessibility的使用(二)
36. UIKit框架(三十六) —— UICollectionView UICollectionViewDiffableDataSource的使用(一)
37. UIKit框架(三十七) —— UICollectionView UICollectionViewDiffableDataSource的使用(二)
38. UIKit框架(三十八) —— 基于CollectionView转盘效果的实现(一)
39. UIKit框架(三十九) —— iOS 13中UISearchController 和 UISearchBar的新更改(一)
40. UIKit框架(四十) —— iOS 13中UISearchController 和 UISearchBar的新更改(二)

开始

首先看下主要内容:

本文主要讲述了如何使用collection protocol来创建自己的Bag collection类型的实现。内容来自翻译

下面是写作环境:

Swift 5, iOS 13, Xcode 11

下面就是正文了

ArrayDictionarySet是Swift标准库中捆绑在一起的常用集合类型。 但是,如果他们没有立即提供您的应用所需的一切,该怎么办? 别担心。 您可以使用Swift标准库中的协议创建自己的自定义集合!

Swift中的集合(Collections)带有大量方便的实用程序,可用于对它们进行迭代,过滤和更多操作。 除了使用自定义集合之外,您还可以将所有业务逻辑添加到自己的代码中。 但是,这会使您的代码肿,难以维护并且无法复制标准库提供的内容。

幸运的是,Swift提供了强大的收集协议(collection protocol),因此您可以创建自己的收集类型,这些收集类型专门为满足应用程序的需求而量身定制。 您只需实现这些协议即可利用Swift集合的强大功能。

在本教程中,您将从头开始构建一个多集multiset(也称为bag)。

在此过程中,您将学习如何:

  • 采用以下协议:Hashable,Sequence,Collection,CustomStringConvertible,ExpressibleByArrayLiteral和ExpressibleByDictionaryLiteral
  • 为您的集合创建自定义初始化。
  • 使用自定义方法改进自定义集合。

是时候开始了!

注意:本教程适用于Swift 5.0。 由于对Swift标准库进行了重大更改,因此无法编译以前的版本。

在起始文件夹中打开文件Bag.playground

注意:如果愿意,可以创建自己的Xcode playground。 如果这样做,请删除所有默认代码以从一个空的playground开始。


Creating the Bag Struct

接下来,将以下代码添加到您的playground

struct Bag<Element: Hashable> {
}

就这样, “Papa’s got a brand new bag”

您的Bag是一种通用结构,需要一个Hashable元素类型。 要求使用Hashable元素可以比较和存储O(1)时间复杂度的唯一值。 这意味着,无论其内容物的大小如何,Bag都将以恒定的速度运行。 另外,请注意,您正在使用struct; 就像Swift对标准集合所做的那样,这会强制执行值语义。

Bag就像Set,因为它不存储重复的值。 所不同的是:Bag保留所有重复值的连续计数,而Set则不保留。

像购物清单一样考虑它。 如果您想要多个,则不要多次列出。 您只需在项目旁边写上您想要的号码。

要对此建模,请将以下属性添加到playground上的Bag中:

// 1
fileprivate var contents: [Element: Int] = [:]

// 2
var uniqueCount: Int {
  return contents.count
}

// 3
var totalCount: Int {
  return contents.values.reduce(0) { $0 + $1 }
}

这些是Bag所需的基本属性。 这是每一步的工作:

  • 1) contents:使用Dictionary作为内部数据结构。 这对于Bag来说非常有用,因为它会强制执行用于存储元素的唯一键。 每个元素的值就是其计数。 请注意,您将此属性标记为fileprivate,以使Bag的内部工作对外界隐藏。
  • 2) uniqueCount:返回唯一商品的数量,忽略其单独数量。 例如,一个包含4个橙子和3个苹果的Bag将返回的uniqueCount2
  • 3) totalCount:返回Bag中的物品总数。 在上面的示例中,totalCount将返回7

Adding Edit Methods

现在,您将实现一些方法来编辑Bag的内容。

1. Adding Add Method

在刚添加的属性下添加以下方法:

// 1
mutating func add(_ member: Element, occurrences: Int = 1) {
  // 2
  precondition(occurrences > 0,
    "Can only add a positive number of occurrences")

  // 3
  if let currentCount = contents[member] {
    contents[member] = currentCount + occurrences
  } else {
    contents[member] = occurrences
  }
}

这是这样做的:

  • 1) add(_:occurrences :):提供一种向Bag添加元素的方法。它带有两个参数:通用类型Element和一个可选的出现次数。您将方法标记为mutating,因此可以修改contents实例变量。
  • 2) precondition(_:_ :):要求大于0次出现。如果此条件为假,则执行停止,并且遵循该条件的String将出现在playground调试器中。
  • 3) 本部分检查bag中是否已存在该元素。如果是这样,它将增加计数。如果没有,它将创建一个新元素。

注意:在本教程中,您将使用precondition,以确保按预期方式使用Bag。您还将使用precondition进行健全性检查,以确保在添加功能时一切正常。逐步执行此操作将使您避免意外破坏以前运行的功能。

现在您已经可以将元素添加到Bag实例中,还需要一种将其删除的方法。

2. Implementing the Remove Method

add(_:occurrences :)下面添加以下方法:

mutating func remove(_ member: Element, occurrences: Int = 1) {
  // 1
  guard 
    let currentCount = contents[member],
    currentCount >= occurrences 
    else {
      return
  }

  // 2
  precondition(occurrences > 0,
    "Can only remove a positive number of occurrences")

  // 3
  if currentCount > occurrences {
    contents[member] = currentCount - occurrences
  } else {
    contents.removeValue(forKey: member)
  }
}

请注意,remove(_:occurrences :)add(_:occurrences :)具有相同的参数。 运作方式如下:

  • 1) 首先,它检查该元素是否存在,并且至少具有调用者要删除的出现次数。 如果不是,则该方法返回。
  • 2) 接下来,确保要删除的出现次数大于0。
  • 3) 最后,它检查元素的当前计数是否大于要删除的出现次数。 如果更大,则通过从当前计数中减去要删除的出现次数来设置元素的新计数。 如果不大,则currentCountoccurrences相等,它将完全删除该元素。

目前Bag并没有做太多事情。 您无法访问其内容,也无法使用任何有用的收集方法(如map,filter等)对您的收藏进行操作。

但是,一切都不会丢失! Swift提供了使Bag成为合法集合所需的工具。 您只需要遵循一些协议即可。


Adopting Protocols

Swift中,协议定义了一组属性和方法,这些属性和方法必须在采用它的对象中实现。 要采用协议,只需在classstruct的定义后添加一个冒号,后跟您要采用的协议名称即可。 声明采用协议后,请在对象上实现所需的变量和方法。 完成后,您的对象将符合协议。

这是一个简单的例子。 当前,Bag对象在Playground的结果侧栏中几乎没有显示任何信息。

将以下代码添加到playground的末尾(结构体外部)以查看Bag的运行情况:

var shoppingCart = Bag<String>()
shoppingCart.add("Banana")
shoppingCart.add("Orange", occurrences: 2)
shoppingCart.add("Banana")
shoppingCart.remove("Orange")

然后按Command-Shift-Enter执行playground

这将创建一个带有少量水果的Bag。 如果您查看playground调试器,则会看到对象类型,但不包含任何内容。


Adopting CustomStringConvertible

幸运的是,Swift仅针对这种情况提供了CustomStringConvertible协议! 在Bag的大括号后添加以下内容:

extension Bag: CustomStringConvertible {
  var description: String {
    return String(describing: contents)
  }
}

符合CustomStringConvertible要求实现一个名为description的单个属性。 此属性返回特定实例的文本表示形式。

您将在这里放置创建代表数据的字符串所需的任何逻辑。 由于Dictionary符合CustomStringConvertible,因此您只需将description调用委托给contents

Command-Shift-Enter再次运行playground

看一下shoppingCart的最新改进的调试信息:

太棒了! 现在,在向Bag添加功能时,您将可以验证其内容。

很好! 您正在创建自己喜欢的强大集合类型的过程中。 接下来是初始化。


Creating Initializers

非常烦人的是,您一次只能添加一个元素。 您应该能够通过传递要添加的对象集合来初始化Bag

将以下代码添加到playground的末尾(但请注意,这尚不能编译):

let dataArray = ["Banana", "Orange", "Banana"]
let dataDictionary = ["Banana": 2, "Orange": 1]
let dataSet: Set = ["Banana", "Orange", "Banana"]

var arrayBag = Bag(dataArray)
precondition(arrayBag.contents == dataDictionary,
  "Expected arrayBag contents to match \(dataDictionary)")

var dictionaryBag = Bag(dataDictionary)
precondition(dictionaryBag.contents == dataDictionary,
  "Expected dictionaryBag contents to match \(dataDictionary)")

var setBag = Bag(dataSet)
precondition(setBag.contents == ["Banana": 1, "Orange": 1],
  "Expected setBag contents to match \(["Banana": 1, "Orange": 1])")

这就是您期望创建Bag的方式。 但是它不会编译,因为您尚未定义一个初始化器来接收其他集合。 您将使用泛型(generics),而不是为每种类型显式创建初始化方法。

Bag实现中的totalCount下方添加以下方法:

// 1
init() { }

// 2
init<S: Sequence>(_ sequence: S) where
  S.Iterator.Element == Element {
  for element in sequence {
    add(element)
  }
}

// 3
init<S: Sequence>(_ sequence: S) where
  S.Iterator.Element == (key: Element, value: Int) {
  for (element, count) in sequence {
    add(element, occurrences: count)
  }
}

这是您刚刚添加的内容:

  • 1) 首先,您创建了一个空的初始化程序。在定义其他init方法时,您需要添加此代码。
  • 2) 接下来,添加了一个初始化程序,该初始化程序接受符合Sequence协议的所有内容,其中该序列的元素与Bag的元素相同。这涵盖了数组Array和集合Set类型。您遍历序列传递的内容,并一次添加一个元素。
  • 3) 此后,您添加了一个类似的初始化程序,但是它接受类型为(Element,Int)的元组。字典就是一个例子。在这里,您遍历序列中的每个元素并添加指定的计数。

再次按Command-Shift-Enter即可运行playground。请注意,您之前添加在底部的代码现在可以使用。

1. Initializing Collections

这些通用的初始化程序为Bag对象启用了更多种类的数据源。但是,它们确实需要您首先创建传递给初始化程序的集合。

为了避免这种情况,Swift提供了两种协议来启用序列文字的初始化。文字(Literals)为您提供了一种无需显式创建对象即可写数据的简便方法。

要看到这一点,首先将以下代码添加到您的playground的末尾:(注意:在添加所需的协议之前,这也会产生错误。)

var arrayLiteralBag: Bag = ["Banana", "Orange", "Banana"]
precondition(arrayLiteralBag.contents == dataDictionary,
  "Expected arrayLiteralBag contents to match \(dataDictionary)")

var dictionaryLiteralBag: Bag = ["Banana": 2, "Orange": 1]
precondition(dictionaryLiteralBag.contents == dataDictionary,
  "Expected dictionaryLiteralBag contents to match \(dataDictionary)")

上面的代码是使用ArrayDictionary文字而不是对象进行初始化的示例。

现在,要使它们起作用,请在CustomStringConvertible扩展下面添加以下两个扩展:

// 1
extension Bag: ExpressibleByArrayLiteral {
  init(arrayLiteral elements: Element...) {
    self.init(elements)
  }
}

// 2
extension Bag: ExpressibleByDictionaryLiteral {
  init(dictionaryLiteral elements: (Element, Int)...) {
    self.init(elements.map { (key: $0.0, value: $0.1) })
  }
}
  • 1) ExpressibleByArrayLiteral用于根据数组样式文字创建Bag。 在这里,您可以使用之前创建的初始化程序,并传入elements集合。
  • 2) ExpressibleByDictionaryLiteral的功能相同,但对于字典样式的文字而言。 该映射将元素转换为初始化程序期望的命名元组。

Bag看起来更像是原生collection类型,是时候尝试真正的魔术了。


Understanding Custom Collections

您现在已经学到了足够的知识,可以理解什么是自定义集合(collection):您定义的集合对象既符合Sequence协议又符合Collection协议。

在上一节中,您定义了一个初始化程序,该初始化程序接受符合Sequence协议的集合对象。 Sequence表示一种类型,该类型提供对其元素的顺序,迭代访问。 您可以将序列视为一系列项目,让您一次遍历每个元素。

There are way too many Pokemon to keep track these days

迭代是一个简单的概念,但是此功能为您的对象提供了巨大的功能。它允许您执行各种强大的操作,例如:

  • map(_ :):使用提供的闭包转换序列中的每个元素后,返回结果数组。
  • filter(_ :):返回满足提供的闭包谓词的元素数组。
  • sorted(by :):返回基于提供的闭包谓词排序的元素数组。

要查看Sequence中可用的所有方法,请查看Apple’s documentation on the Sequence Protocol

1. Enforcing Non-destructive Iteration

一个警告:Sequence不需要符合性的类型是非破坏性的。这意味着迭代后,无法保证以后的迭代会从头开始。如果您计划多次迭代数据,那将是一个巨大的问题。

要实施非破坏性迭代,您的对象需要符合Collection协议。

Collection继承自IndexableSequence

主要区别在于,集合是可以多次遍历并按索引访问的序列。

遵循Collection,您将免费获得许多方法和属性。 一些例子是:

  • isEmpty:返回一个布尔值,指示集合是否为空。
  • first:返回集合中的第一个元素。
  • count:返回集合中元素的数量。

根据集合中元素的类型,还有更多可用的方法。 在Apple的Apple’s documentation on the Collection Protocol中查看它们。

抓住你的Bag,并采用这些协议!


Adopting the Sequence Protocol

对集合类型执行的最常见操作是遍历其元素。 例如,将以下内容添加到playground的末尾:

for element in shoppingCart {
  print(element)
}

ArrayDictionary一样,您应该能够遍历Bag。 由于当前的Bag类型不符合Sequence,因此无法编译。

现在修复该问题。

1. Conforming to Sequence

ExpressibleByDictionaryLiteral扩展之后添加以下内容:

extension Bag: Sequence {
  // 1
  typealias Iterator = DictionaryIterator<Element, Int>

  // 2
  func makeIterator() -> Iterator {
    // 3
    return contents.makeIterator()
  }
}

并不需要太多符合Sequence。 在上面的代码中,您:

  • 1) 创建名为IteratorTypealias作为DictionaryIteratorSequence要求知道如何迭代序列。 DictionaryIteratorDictionary对象用来迭代其元素的类型。 您之所以使用这种类型,是因为Bag将其基础数据存储在Dictionary中。
  • 2) 将makeIterator()定义为返回Iterator的方法,以逐步浏览序列中的每个元素。
  • 3) 通过委派contents上的makeIterator()来返回迭代器,该内容本身符合Sequence

这就是使Bag符合Sequence所需的全部!

您现在可以遍历Bag的每个元素,并获取每个对象的计数。 在上一个for-in循环之后,将以下内容添加到playground的末尾:

for (element, count) in shoppingCart {
  print("Element: \(element), Count: \(count)")
}

Command-Shift-Enter运行playground。 打开playground控制台,您将按顺序看到元素的打印输出及其数量。

2. Viewing Benefits of Sequence

能够遍历一个Bag可以启用Sequence实现的许多有用方法。 将以下内容添加到playground的末端,以查看其中的一些操作:

// Find all elements with a count greater than 1
let moreThanOne = shoppingCart.filter { $0.1 > 1 }
moreThanOne
precondition(
  moreThanOne.first!.key == "Banana" && moreThanOne.first!.value == 2,
  "Expected moreThanOne contents to be [(\"Banana\", 2)]")

// Get an array of all elements without their counts
let itemList = shoppingCart.map { $0.0 }
itemList
precondition(
  itemList == ["Orange", "Banana"] ||
    itemList == ["Banana", "Orange"],
  "Expected itemList contents to be [\"Orange\", \"Banana\"] or [\"Banana\", \"Orange\"]")

// Get the total number of items in the bag
let numberOfItems = shoppingCart.reduce(0) { $0 + $1.1 }
numberOfItems
precondition(numberOfItems == 3,
  "Expected numberOfItems contents to be 3")

// Get a sorted array of elements by their count in descending order
let sorted = shoppingCart.sorted { $0.0 < $1.0 }
sorted
precondition(
  sorted.first!.key == "Banana" && moreThanOne.first!.value == 2,
  "Expected sorted contents to be [(\"Banana\", 2), (\"Orange\", 1)]")

Command-Shift-Enter键可以运行playground,并查看它们的运行情况。

这些都是使用序列的有用方法 - 您实际上是免费获得的!

现在,您可能对Bag的使用方式感到满意,但是这样做的乐趣在哪里? 您绝对可以改善当前的Sequence实现。


Improving Sequence

当前,您依靠Dictionary为您处理繁重的工作。 很好,因为它使创建自己的强大集合变得容易。 问题在于它为Bag用户带来了奇怪而令人困惑的情况。 例如,Bag返回类型为DictionaryIterator的迭代器并不直观。

但是,Swift再次来了! Swift提供类型AnyIterator来隐藏底层的迭代器。

用以下内容替换Sequence扩展的实现:

extension Bag: Sequence {
  // 1
  typealias Iterator = AnyIterator<(element: Element, count: Int)>

  func makeIterator() -> Iterator {
    // 2
    var iterator = contents.makeIterator()

    // 3
    return AnyIterator {
      return iterator.next()
    }
  }
}

在此修订的Sequence扩展中,您:

  • 1) 将Iterator定义为符合AnyIterator,而不是DictionaryIterator。 然后,像以前一样,创建makeIterator()返回一个Iterator
  • 2) 通过在contents上调用makeIterator()创建Iterator。 下一步需要此变量。
  • 3) 将Iterator包装在新的AnyIterator对象中,以转发其next()方法。 next()方法是在迭代器上调用的方法,用于获取序列中的下一个对象。

Command-Shift-Enter运行playground。 您会注意到几个错误:

以前,您在使用DictionaryIterator时使用了keyvalue的元组名称。 您已经从外界隐藏了DictionaryIterator,并将暴露的元组名称重命名为elementcount

要修复错误,请分别将keyvalue替换为elementcount。 立即运行playground,您的precondition块将像以前一样通过。

现在没有人会知道您只是在使用Dictionary为您辛苦工作!

是时候将您的Bag带回家了。


Adopting the Collection Protocol

事不宜迟,这里是创建集合的真正内容:集合(Collection)协议! 重申一下,集合Collection是一个序列,您可以按索引对其进行访问并多次遍历。

要采用Collection,您需要提供以下详细信息:

  • startIndex和endIndex:定义集合的边界,并公开横向的起点。
  • subscript (position:):允许使用索引访问集合中的任何元素。 此访问应以O(1)时间复杂度运行。
  • index(after :):在传入索引之后立即返回索引。

拥有有效的收藏集仅需四个细节。

Sequence扩展之后添加以下代码:

extension Bag: Collection {
  // 1
  typealias Index = DictionaryIndex<Element, Int>

  // 2
  var startIndex: Index {
    return contents.startIndex
  }

  var endIndex: Index {
    return contents.endIndex
  }

  // 3
  subscript (position: Index) -> Iterator.Element {
    precondition(indices.contains(position), "out of bounds")
    let dictionaryElement = contents[position]
    return (element: dictionaryElement.key,
      count: dictionaryElement.value)
  }

  // 4
  func index(after i: Index) -> Index {
    return contents.index(after: i)
  }
}

这很简单。 在这里,您:

  • 1) 将Collection中定义的Index类型声明为DictionaryIndex。 您会将这些索引传递给内容。
  • 2) 从contents返回开始和结束索引。
  • 3) 使用precondition来强制执行有效索引。 您从该索引处的contents返回值作为新的元组。
  • 4) 返回在contents上调用的index(after :)的值。

通过简单地添加这些属性和方法,您就创建了一个功能齐全的集合!

1. Testing Your Collection

将以下代码添加到playground的末尾以测试一些新功能:

// Get the first item in the bag
let firstItem = shoppingCart.first
precondition(
  (firstItem!.element == "Orange" && firstItem!.count == 1) ||
  (firstItem?.element == "Banana" && firstItem?.count == 2),
  "Expected first item of shopping cart to be (\"Orange\", 1) or (\"Banana\", 2)")

// Check if the bag is empty
let isEmpty = shoppingCart.isEmpty
precondition(isEmpty == false,
  "Expected shopping cart to not be empty")

// Get the number of unique items in the bag
let uniqueItems = shoppingCart.count
precondition(uniqueItems == 2,
  "Expected shoppingCart to have 2 unique items")

// Find the first item with an element of "Banana"
let bananaIndex = shoppingCart.indices.first { 
  shoppingCart[$0].element == "Banana"
}!
let banana = shoppingCart[bananaIndex]
precondition(banana.element == "Banana" && banana.count == 2,
  "Expected banana to have value (\"Banana\", 2)")

再次运行playground。 太棒了!

提示一下您对所做的事情感到非常满意的那一刻,但感觉到即将出现"but wait, you can do better"的评论……嗯,您是对的! 你可以做得更好。 您的Bag中仍有一些Dictionary


Improving Collection

Bag又回到了太多的内部运作。 Bag的用户需要使用DictionaryIndex对象来访问集合中的元素。

您可以轻松解决此问题。 在Collection扩展名后面添加以下内容:

// 1
struct BagIndex<Element: Hashable> {
  // 2
  fileprivate let index: DictionaryIndex<Element, Int>

  // 3
  fileprivate init(
    _ dictionaryIndex: DictionaryIndex<Element, Int>) {
    self.index = dictionaryIndex
  }
}

在上面的代码中,您:

  • 1) 定义一个新的通用类型BagIndex。 像Bag一样,这需要可用于字典的Hashable泛型类型。
  • 2) 使该索引类型的基础数据成为DictionaryIndex对象。 BagIndex实际上只是一个包装,将其真实索引对外界隐藏。
  • 3) 创建一个接受DictionaryIndex进行存储的初始化程序。

现在,您需要考虑以下事实,即Collection要求Index具有可比性,以允许比较两个索引来执行操作。 因此,BagIndex需要采用Comparable

BagIndex之后添加以下扩展名:

extension BagIndex: Comparable {
  static func ==(lhs: BagIndex, rhs: BagIndex) -> Bool {
    return lhs.index == rhs.index
  }

  static func <(lhs: BagIndex, rhs: BagIndex) -> Bool {
    return lhs.index < rhs.index
  }
}

这里的逻辑很简单; 您正在使用DictionaryIndex的等效方法返回正确的值。


Updating BagIndex

现在,您可以准备将Bag更新为使用BagIndex。 将Collection扩展替换为以下内容:

extension Bag: Collection {
  // 1
  typealias Index = BagIndex<Element>

  var startIndex: Index {
    // 2.1
    return BagIndex(contents.startIndex)
  }

  var endIndex: Index {
    // 2.2
    return BagIndex(contents.endIndex)
  }

  subscript (position: Index) -> Iterator.Element {
    precondition((startIndex ..< endIndex).contains(position),
      "out of bounds")
    // 3
    let dictionaryElement = contents[position.index]
    return (element: dictionaryElement.key,
      count: dictionaryElement.value)
  }

  func index(after i: Index) -> Index {
    // 4
    return Index(contents.index(after: i.index))
  }
}

每个带编号的注释都表示更改。它们是:

  • 1) 将Index类型从DictionaryIndex替换为BagIndex
  • 2) 从startIndexendIndexcontents创建一个新的BagIndex
  • 3) 使用BagIndexindex属性访问contents并从中返回元素。
  • 4) 使用BagIndex的属性从内容获取DictionaryIndex值,并使用该值创建一个新的BagIndex

就这些!用户回到对存储数据的方式一无所知。您还可能会更好地控制索引对象。

在总结之前,还有一个更重要的主题需要讨论。通过添加基于索引的访问,您现在可以为集合中的一系列值建立索引。是时候让您了解slice如何与集合一起工作了。


Using Slices

slice是视图集合中元素的子序列。它使您无需复制就可以对元素的特定子序列执行操作。

slice存储对创建它的基础集合的引用。slice与它们的基本集合共享索引,保留对开始和结束索引的引用以标记子序列范围。slice具有O(1)空间复杂度,因为它们直接引用其基本集合。

要查看其工作原理,请将以下代码添加到playground的末尾:

// 1
let fruitBasket = Bag(dictionaryLiteral:
  ("Apple", 5), ("Orange", 2), ("Pear", 3), ("Banana", 7))

// 2
let fruitSlice = fruitBasket.dropFirst()

// 3
if let fruitMinIndex = fruitSlice.indices.min(by:
  { fruitSlice[$0] > fruitSlice[$1] }) {
  // 4
  let basketElement = fruitBasket[fruitMinIndex]
  let sliceElement = fruitSlice[fruitMinIndex]
  precondition(basketElement == sliceElement,
    "Expected basketElement and sliceElement to be the same element")
}

再次运行playground

在上面的代码中,您:

  • 1) 创建一个由四个不同水果组成的水果篮。
  • 2) 移除第一类水果。实际上,这只是在水果篮中创建一个新的slice视图(不包括您删除的第一个元素),而不是创建一个全新的Bag对象。您会在结果栏中注意到这里的类型为Slice <Bag <String >>
  • 3) 在剩余的水果中找到最少出现的水果的索引。
  • 4) 证明即使从slice计算索引,您也可以使用基础集合和切片中的索引来检索相同的元素。

注意:对于基于哈希的集合(例如DictionaryBag),切片似乎没什么用,因为它们的顺序未以任何有意义的方式定义。另一方面,Array是集合类型的一个很好的例子,其中切片在执行子序列操作中起着巨大的作用。

在本教程中,您学习了如何在Swift中创建自定义集合。您对 SequenceCollectionCustomStringConvertibleExpressibleByArrayLiteralExpressibleByDictionaryLiteral添加了一致性,并创建了自己的索引类型。

如果您想查看或为更完整的Bag实施做出贡献,请查看Swift Algorithm Club implementation实施以及Foundation实施NSCountedSet

这些只是Swift提供的用于创建健壮和有用的集合类型的所有协议的一种体验。如果您想了解一些此处未涵盖的内容,请查看以下内容:

您还可以查看有关Protocols in Swift的更多信息,并了解有关采用Swift标准库中可用的通用协议adopting common protocols的更多信息。

后记

本篇主要讲述了使用协议构建自定义Collection,感兴趣的给个赞或者关注~~~

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

推荐阅读更多精彩内容