版本记录
版本号 | 时间 |
---|---|
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
下面就是正文了
Array
,Dictionary
和Set
是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 {
}
就这样, “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
将返回的uniqueCount
为2
。 - 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) 最后,它检查元素的当前计数是否大于要删除的出现次数。 如果更大,则通过从当前计数中减去要删除的出现次数来设置元素的新计数。 如果不大,则
currentCount
和occurrences
相等,它将完全删除该元素。
目前Bag
并没有做太多事情。 您无法访问其内容,也无法使用任何有用的收集方法(如map,filter
等)对您的收藏进行操作。
但是,一切都不会丢失! Swift
提供了使Bag
成为合法集合所需的工具。 您只需要遵循一些协议即可。
Adopting Protocols
在Swift
中,协议定义了一组属性和方法,这些属性和方法必须在采用它的对象中实现。 要采用协议,只需在class
或struct
的定义后添加一个冒号,后跟您要采用的协议名称即可。 声明采用协议后,请在对象上实现所需的变量和方法。 完成后,您的对象将符合协议。
这是一个简单的例子。 当前,Bag
对象在Playground
的结果侧栏中几乎没有显示任何信息。
将以下代码添加到playground
的末尾(结构体外部)以查看Bag
的运行情况:
var shoppingCart = Bag()
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(_ sequence: S) where
S.Iterator.Element == Element {
for element in sequence {
add(element)
}
}
// 3
init(_ 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)")
上面的代码是使用Array
和Dictionary
文字而不是对象进行初始化的示例。
现在,要使它们起作用,请在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
表示一种类型,该类型提供对其元素的顺序,迭代访问。 您可以将序列视为一系列项目,让您一次遍历每个元素。
迭代是一个简单的概念,但是此功能为您的对象提供了巨大的功能。它允许您执行各种强大的操作,例如:
- map(_ :):使用提供的闭包转换序列中的每个元素后,返回结果数组。
- filter(_ :):返回满足提供的闭包谓词的元素数组。
- sorted(by :):返回基于提供的闭包谓词排序的元素数组。
要查看Sequence
中可用的所有方法,请查看Apple’s documentation on the Sequence Protocol。
1. Enforcing Non-destructive Iteration
一个警告:Sequence
不需要符合性的类型是非破坏性的。这意味着迭代后,无法保证以后的迭代会从头开始。如果您计划多次迭代数据,那将是一个巨大的问题。
要实施非破坏性迭代,您的对象需要符合Collection
协议。
Collection
继承自Indexable
和Sequence
主要区别在于,集合是可以多次遍历并按索引访问的序列。
遵循Collection
,您将免费获得许多方法和属性。 一些例子是:
- isEmpty:返回一个布尔值,指示集合是否为空。
- first:返回集合中的第一个元素。
- count:返回集合中元素的数量。
根据集合中元素的类型,还有更多可用的方法。 在Apple的Apple’s documentation on the Collection Protocol中查看它们。
抓住你的Bag
,并采用这些协议!
Adopting the Sequence Protocol
对集合类型执行的最常见操作是遍历其元素。 例如,将以下内容添加到playground
的末尾:
for element in shoppingCart {
print(element)
}
与Array
和Dictionary
一样,您应该能够遍历Bag
。 由于当前的Bag
类型不符合Sequence
,因此无法编译。
现在修复该问题。
1. Conforming to Sequence
在ExpressibleByDictionaryLiteral
扩展之后添加以下内容:
extension Bag: Sequence {
// 1
typealias Iterator = DictionaryIterator
// 2
func makeIterator() -> Iterator {
// 3
return contents.makeIterator()
}
}
并不需要太多符合Sequence
。 在上面的代码中,您:
- 1) 创建名为
Iterator
的Typealias
作为DictionaryIterator
。Sequence
要求知道如何迭代序列。DictionaryIterator
是Dictionary
对象用来迭代其元素的类型。 您之所以使用这种类型,是因为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
时使用了key
和value
的元组名称。 您已经从外界隐藏了DictionaryIterator
,并将暴露的元组名称重命名为element
和count
。
要修复错误,请分别将key
和value
替换为element
和count
。 立即运行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
// 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 {
// 2
fileprivate let index: DictionaryIndex
// 3
fileprivate init(
_ dictionaryIndex: DictionaryIndex) {
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
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) 从
startIndex
和endIndex
的contents
创建一个新的BagIndex
。 - 3) 使用
BagIndex
的index
属性访问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
。> - 3) 在剩余的水果中找到最少出现的水果的索引。
- 4) 证明即使从
slice
计算索引,您也可以使用基础集合和切片中的索引来检索相同的元素。
注意:对于基于哈希的集合(例如
Dictionary
和Bag
),切片似乎没什么用,因为它们的顺序未以任何有意义的方式定义。另一方面,Array
是集合类型的一个很好的例子,其中切片在执行子序列操作中起着巨大的作用。
在本教程中,您学习了如何在Swift
中创建自定义集合。您对 Sequence,Collection,CustomStringConvertible,ExpressibleByArrayLiteral,ExpressibleByDictionaryLiteral添加了一致性,并创建了自己的索引类型。
如果您想查看或为更完整的Bag
实施做出贡献,请查看Swift Algorithm Club implementation实施以及Foundation
实施NSCountedSet。
这些只是Swift
提供的用于创建健壮和有用的集合类型的所有协议的一种体验。如果您想了解一些此处未涵盖的内容,请查看以下内容:
- Array Relationships
- Dictionary Relationships
- BidirectionalCollection Relationships
您还可以查看有关Protocols in Swift的更多信息,并了解有关采用Swift标准库中可用的通用协议adopting common protocols的更多信息。
后记
本篇主要讲述了使用协议构建自定义Collection,感兴趣的给个赞或者关注~~~