创建 Swift 自定义集合类
数组、字典和集合是常见的集合类型,它们都内置在 Swift 标准库中。但如果它们不能满足你的 App 的需要的时候怎么办? 一种最常见的办法是使用 Array 或 Dictionary,然后用一堆业务逻辑去保存你的数据结构。但这种方式太过于直接且难于维护。 这样,创建自定义集合类型就变得有意义了。在本文,你将学习用 Swift 的 collection 协议创建自定义集合类型。 当文本结束,你会拥有一个强大的自定义集合类型,拥有 Swift 内置集合的所有功能。
开始在本文中,你将从头开始创建一个“多集合”(Bag)类型。 一个 Bag 对象就像一个 Set,用于存储不会重复的对象。在一个 Set 集合中,重复对象会被忽略。在一个 Bag 中,每个对象都会被算进去。 一个好例子是购物清单。你拥有一个清单,每个商品都和一个数量关联。如果添加了重复的商品,则我们会增加已有商品的数量,而不是重新插入一条商品记录。 在介绍 collection 协议前,首先来实现一个基本的 Bag。 编辑 playground 文件为: struct Bag<Element: Hashable> { }
Bag 结构是一个泛型结构,需要元素类型必须是 Hashable 的。Hashable 允许你对元素进行比较,在 0(1)时间复杂度上只存储唯一值。也就是说,无论内容有多复杂,Bag 存取速度相同。你通过定义一个结构体,强制让它具备值语义(C++ 术语),这就和 Swift 标准库保持一致了。 然后为 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 的基本属性:
现在,需要几个方法以便增减 Bag 中的内容。在属性声明下面加入: // 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
}
}
代码解释如下:
另外,你还需要一个删除元素的方法。在 add 方法后新增方法: mutating func remove(_ member: Element,occurrences: Int = 1) {
// 1
guard let currentCount = contents[member],currentCount >= occurrences else {
preconditionFailure("Removed non-existent elements")
}
// 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 方法一模一样,只不过做了相反的事情:
这里,Bag 还不能干更多的事情,甚至无法访问它的内容。你还不能使用 Dictionary 中存在的高阶方法。 但亡羊补牢为时未晚。我们开始在 Bag 中一一添加这些代码。现在的任务是保持你的代码整洁。 请先等一下!Swift 提供了让 Bag 符合传统集合的所有工具。 自定义集合需要做些什么?要理解什么是 Swift 集合,首先需要它继承的协议层次: Sequence 协议表示类型支持排序、以迭代的方式访问其元素。你可以把一个 Sequence 对象视作一个元素的列表,允许你挨个挨个地访问其中的元素。 迭代(Iteration)是一个简单概念,但它能给你的对象提供许多功能。它允许你各种强大的操作比如:
这只是其中很少的一部分功能。要查看 Sequence 中提供的所有方法,请查看 Sequence 的文档。 需要说明的一点是,采用 Sequence 协议的类型强制要求是破坏性的或者是非破坏性的。这意味着,在迭代之后,无法保证下一次迭代会从头开始。 这是一个大问题,如果你的数据准备迭代不止一次的话。要实现非破坏性的迭代,你的对象需要使用 Collection 协议。 Collection 协议继承了 Sequence 和 Indexable 协议。Collection 和 Sequence 的主要区别是,你可以迭代多次,而且可以用索引来访问。 实现 Collection 协议之后,你会获得更多“免费”的方法和属性,例如:
依据集合中的元素类型的不同,你还可能拥有更多的方法和属性。如果你想了解更多,请查看Collection 的文档。 在实现这些协议之前,Bag 还有一个地方需要改进。 打印对象当前,Bag 对象可以用 print(_:) 方法或在 Result Sidebar 视图中暴露出的信息很少。 var shoppingCart = Bag<String>()
shoppingCart.add("Banana")
shoppingCart.add("Orange",occurrences: 2)
shoppingCart.add("Banana")
shoppingCart.remove("Orange")
这里创建了一个 Bag 对象,并加入了几种水果。如果你查看Playground 的调试窗口,你会看到这些对象的类型信息而不是它保存的内容。 你可以用 Swift 标准库中的一个协议来解决这个问题。在 shoppingCart 变量上面的 Bag 类型定义结束的 } 之后添加: extension Bag: CustomStringConvertible {
var description: String {
return String(describing: contents)
}
}
采用 CustomStringConvertible 协议需要实现一个属性,叫做 description。这个属性返回一个实例对象的文字表示。 在这里,你可以放入任何足以表示你的数据的逻辑。因为字典已经继承了这个协议,你可以简单调用 contents 对象的 description 值。 看一眼 shopingCart 的 debug 信息: 漂亮!现在你已经为 Bag 添加了功能,你可以对它的 contents 进行校验了。 在 Playground 中编写代码时,你可以使用 precondition(_:_:) 来检验返回结果。这会避免你突然破坏之前编写的功能。可以用这个工具作为你的单元测试——将它放到你的日常编码中去做是一个不错的主意! 在最后一次调用 remove(_:occurrences:) 之后加入: precondition("(shoppingCart)" == "(shoppingCart.contents)","Expected bag description to match its contents description")
如果 shoppingCart 的 description 属性不等于 contents 的 description,则会导致一个错误。 为了创建我们屌爆了的集合类型,接下来的步骤自然就是初始化。 初始化每次只能加一个元素真的很烦。通常的办法是在初始化的时候用另一个集合来进行初始化。 这正是我们希望 Bag 能够做到的。在 Playground 的最后加入: let dataArray = ["Banana","Orange","Banana"]
let dataDictionary = ["Banana": 2,"Orange": 1]
let dataSet: Set = ["Banana","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])")
无法进行编译,因为还没有定义针对这些类型的初始化方法。不要为每种类型创建一种初始化方法,你可以使用泛型。 // 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)
}
}
代码解释如下:
这些泛型初始化方法为 Bag 对象添加了大量的数据源。但是,它们仍然你初始化另一个 Sequence 对象然后传递给 Bag。 为了避免这个,Swift 标准库提供了两个协议。这两个协议支持以 Sequence 的写法进行初始化。这种写法能让你用更简短的方式定义数据,而不必显式地创建一个对象。 在 Playground 最后加入下列代码,来看看如何使用这种写法: var arrayLiteralBag: Bag = ["Banana","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)")
没说的,编译器报错了,我们后面再来解决。这种写法用初始化数组和字典的写法来进行初始化,而不需要创建对象。 在 Bag 的其它扩展之后定义两个扩展: extension Bag: ExpressibleByArrayLiteral {
init(arrayLiteral elements: Element...) {
self.init(elements)
}
}
extension Bag: ExpressibleByDictionaryLiteral {
init(dictionaryLiteral elements: (Element,Int)...) {
// The map converts elements to the "named" tuple the initializer expects.
self.init(elements.map { (key: $0.0,value: $0.1) })
}
}
ExpressibleByArrayLiteral 和 ExpressibleByDictionaryLiteral 扩展需要实现一个初始化方法,以处理它们对应的参数的那种写法。由于前面已经定义的初始化方法,它们的实现都非常简单。 现在 Bag 已经非常像原生的集合类型了,我们该来点猛货了。 Sequence集合类型的最常用的操作是对其元素进行迭代。来看一个例子,在 Playground 最后添加如下代码: for element in shoppingCart {
print(element)
}
超级简单。就像数组和字典一样,你可以遍历一个 Bag 对象。因为 Bag 还没有实现 Sequence 协议,编译不能通过。 extension Bag: Sequence {
// 1
typealias Iterator = DictionaryIterator<Element,Int>
// 2
func makeIterator() -> Iterator {
// 3
return contents.makeIterator()
}
}
不需要实现太多方法。代码解释如下:
这就是 Bag 对 Sequence 协议进行实现的全部。 for (element,count) in shoppingCart {
print("Element: (element),Count: (count)")
}
打开 Debug 视图,你会看到每个元素都打印出来了: 实现 Sequence 协议之后,你就可以使用许多 Sequence 中有的方法了。 在 Playground 最后加入代码试一试: // 查找所有数目大于 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)]")
// 获取所有对象的数组(不需要数量)
let itemList = shoppingCart.map { $0.0 }
itemList
precondition(itemList == ["Orange","Banana"],"Expected itemList contents to be ["Orange","Banana"]")
// 获得所有对象的加总数据
let numberOfItems = shoppingCart.reduce(0) { $0 + $1.1 }
numberOfItems
precondition(numberOfItems == 3,"Expected numberOfItems contents to be 3")
// 所有商品按照数量降序排序
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)]")
所有 Sequence 对象能有的方法都能用——它们完全是免费的。 现在,你可能满足于以这种方式使用 Bag,但还有比这更好玩的吗?当前的 Squence 实现仍然还有改进的余地。 加强版的 Sequence当前,你依赖于字典为你提供底层支持。这很好,对你来说,这不乏为一种轻松实现功能强大的集合的方式。问题是,它会让 Bag 的用户感到奇怪和困惑。 例如,Bag 会返回一个 DictionaryIterator 类型的 Iterator 好像不妥。你可以创建自己的 Iterator 类型,但这次不是免费的了。 Swift 提供了一个 AnyIterator 类型,将底层的 itertator 隐藏起来。 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()
}
}
}
Playground 报了一堆错,等会来解决。除了使用了一个 AnyIterator 外,这个实现和之前没有太大区别:
现在来解决先前的报错。你看到的是这两个错误: 前面,你用 DictionaryIterator 的元组命名为 key 和 value。现在你已经将 DictionaryIterator 隐藏起来了,并且将元组的名字修改为 element 和 count。要解决这个错误,将 key 和 value 替换成 element 和 count。 这样你的 precondition 语句的问题就解决了。这就是前置条件的好处了,它能保证某些东西不会被意外修改。 现在,任何人都不知道你在用字典进行所有的工作。 Collectdion闲话少说,接下来还有一道大菜,创建一个集合……即 Collection 协议!再次声明,集合是能够通过索引进行访问并进行多次非破坏性迭代的结合。 为了符合 Collection 协议,你需要提供这些数据:
使用 Cellection 协议时只需要这 4 个数据。在 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)
}
}
代码非常简单:
通过添加几个属性和方法,你创建了一个功能完整的集合!在 Playground 最后用几行代码来测试这些功能: // 读取 Bag 中的第一个对象
let firstItem = shoppingCart.first
precondition(firstItem!.element == "Orange" && firstItem!.count == 1,"Expected first item of shopping cart to be ("Orange",1)")
// 判断 Bag 是否为空
let isEmpty = shoppingCart.isEmpty
precondition(isEmpty == false,"Expected shopping cart to not be empty")
// 获取 Bag 中的商品种类数
let uniqueItems = shoppingCart.count
precondition(uniqueItems == 2,"Expected shoppingCart to have 2 unique items")
// 查找第一个名为 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)")
漂亮!(当你对自己所做的一切感到心满意足时,“等等,你还可以做得更好”这句话又在等着你了……) 是的,你说的没错!你可以做得更好。仍然能够从 Bag 中看出一丝字典的模样。 加强版的 CollectionBag 暴露了太多底层实现细节。Bag 的用户仍然需要使用 DictionaryIndex 对象去访问集合中的元素。 // 1
struct BagIndex<Element: Hashable> {
// 2
fileprivate let index: DictionaryIndex<Element,Int>
// 3
fileprivate init(_ dictionaryIndex: DictionaryIndex<Element,Int>) {
self.index = dictionaryIndex
}
}
没有任何新奇的玩意儿,但我们还是来过一下吧:
Collection 对象需要索引对象实现 Comparable 协议,能够对两个索引进行比较以进行某些操作。因此,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 已实现的 Comparable 协议的相同方法。 现在将 Bag 修改为使用 BagIndex。将 Collecdtion 扩展替换成: 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
let fruitBasket = Bag(dictionaryLiteral: ("Apple",5),("Orange",2),("Pear",3),("Banana",7))
// 2
let fruitSlice = fruitBasket.dropFirst() // No pun intended ;]
// 3
if let fruitMinIndex = fruitSlice.indices.min(by: { fruitSlice[$0] > fruitSlice[$1] }) {
// 4
let minFruitFromSlice = fruitSlice[fruitMinIndex]
let minFruitFromBasket = fruitBasket[fruitMinIndex]
}
我们来看一下这些代码做了些什么,以及它们的意思:
祝贺你——你现在是一个集合方面的专家了!你可以创建任意内容的 Bag 对象来表示庆祝。 结束完整的 Playground 代码可以在这里下载。如果你想了解或者实现更完整的 Bag,请从 github 中 checkout 项目。 在这篇文章里,你学习了在 Swift 中,如何通过一个数据结构来创建自己的集合。你使用了 Sequence 、Collection 、CustomStringConvertible、 ExpressibleByArrayLiteral、ExpressibleByDictionaryLiteral 协议以及资第一的索引类型。 这仅仅是对 Swift 提供的用于创建健壮、使用的结合类型的协议的一点尝试。如果你想看一下还有什么其他的协议,你可以参考:
希望你能喜欢这篇教程!创建自定义的集合并不是一个常见的需求,当它能加深你对 Swift 内置集合类型的裂解。 如果有任何疑问或建议,请在下面留言。 (编辑:李大同) 【声明】本站内容均来自网络,其相关言论仅代表作者个人观点,不代表本站立场。若无意侵犯到您的权利,请及时与联系站长删除相关内容! |