[ WWDC2018 ] – 高效用集合 Using Collections Effectively

作者 : 开心源码 本文共5004个字,预计阅读时间需要13分钟 发布时间: 2022-05-11 共70人阅读

一个没有集合的世界

如果世界没有集合

如果没有Array

如果, 我们要定义一个熊, 我们能

let bear1 = "Grizzly" 

假如, 我们要四个呢?

let bear1 = "Grizzly" let bear2 = "Panda" let bear3 = "Polar" let bear4 = "Spectacled" 

现在, 让我们尝试打印出来

let bear1 = "Grizzly" let bear2 = "Panda" let bear3 = "Polar" let bear4 = "Spectacled" print("\(bear1) bear") // Grizzly bear print("\(bear2) bear") // Panda bearprint("\(bear3) bear") // Polar bear print("\(bear4) bear") // Spectacled bear 

我们需要不断的做重复的事情

如果没有Dictionary

让我们继续上面的例子, 现在我们有一个记录每个熊的喜好的函数

func habitat(for bear: String) -> String? {     if bear == "Polar" {         return "Arctic"     } else if bear == "Grizzly" {         return "Forest"     } else if bear == "Brown" {         return "Forest"     } else if /* all the other bears */     ...     return nil } 

我们依然有很多重复的事情需要做.

一个拥有集合的世界

当我们引入集合的概念, 上面的事情, 就变得清晰明亮了

let bear = ["Grizzly", "Panda", "Polar", "Spectacled"] let habitats = ["Grizzly": "Forest", "Polar": "Arctic"] for bear in bears {     print("\(bear) Bear") } let bear = bears[2] let habitat = habitats[bear] ?? "" print("\(bear) bears live in the \(habitat)") 

集合有很多相同的行为和属性, 于是我们把它们笼统出来, 作为集合协议.

访问集合

集合有很多应使用, 例如链表, 红黑树等等, 他们都有一个起始位置, 一个终止位置, 能通过下表访问任意位置的元素

WX20180614-204452@2x.png

代码展现

protocol Collection : Sequence {         // 集合中元素的类型    associatedtype Element         // 索引类型, 需要遵守Comparable协议    associatedtype Index : Comparable         // 遍历时所使用到的方法了, 即通过索引查询到对应的元素    subscript(position: Index) -> Element { get }     // 开始索引    var startIndex: Index { get }     // 结束索引    var endIndex: Index { get }     // 通过一个索引, 获取它后面的索引    func index(after i: Index) -> Index } 

这里使用到了associatedtype关键字, 在Swift协议定义的时候, 会看到用这个关键字, 你能认为这是一个占位符, 具体的类型直到被使用到的时候才会确定. 但是有时候我们需要规定这个占位符要有少量可以力, 比方这里的Index, 他就需要遵守Comparable协议.

接下来的一个方法, 是通过索引访问到对应的元素

startIndex, endIndex, 表示了集合的边界

最后这个方法, 能通过索引来获取下一个元素的索引.

集合的扩展

WX20180614-210500@2x.png

这张图是少量集合的扩展, 放眼望去, 有少量我们会经常使用到的方法或者属性, 例如:

  • first, 集合的第一个元素
  • last, 集合的最后一个元素
  • isEmpty, 集合能否是空的
  • count, 集合元素个数

使用于遍历的

  • forEach
  • makeIterator()

少量高阶函数

  • map
  • filter
  • reduce

当然, 我们也能做少量自己的扩展

扩展DIY

系统提供的遍历是一一元素遍历, 现在, 让我们来实现一个隔元素访问的功可以.

WX20180614-211836@2x.png

extension Collection { // 扩展集合协议    func everyOther(_ body: (Element) -> Void) {         // 获取首元素索引        let start = self.startIndex         // 获取末尾元素索引        let end = self.endIndex         var iter = start         // 未走到末尾        while iter != end {            // 执行外部的闭包             body(self[iter])            // 获取当前元素的下一个索引            let next = index(after: iter)             // 索引能否走到末尾            if next == end { break }             // 将当前索引指向next的下一个            iter = index(after: next)         }     } } (1...10).everyOther { print($0) } 

继承关系

WX20180614-212511@2x.png

实际上, 除了Collection以外, 我们还有很多继承自Collection的协议, 例如,

  • BidirectionalCollection 双向集合, 能向前访问元素, 当然, 它继承自Collection, 也能向后访问元素.

  • RandomAccessCollection 随机访问集合, 提供了复杂度为O(1)的访问方法, 当然, 它也有向前和向后访问元素的可以力

  • MutableCollection 可变集合, 提供了修改集合元素的可以力

  • RangeReplaceCollection 范围替换集合, 提供了通过指定范围替换元素的可以力

索引

我们能通过索引的方式来访问集合中的元素, 例如, 我们要访问集合中的第一个元素,

访问第一个元素

通过下标进行直接访问

用array[0]访问第一个元素, 当然没有问题, 可是假如我们扩开展来, 假如给的集合不是数组, 而是一个set, 那么, 这样的方式就行不通了.

通过索引进行访问

用set[set.startIndex]进行访问, 这样即可以了, 但是, 你需要注意潜在的问题, 你需要判空, 需要判断越界, 诸如此类

first

好在苹果的工程师为这些常使用的元素访问留了方便的方式我们能用set.first进行获取. 而且不使用担心那些潜在的问题

访问第二个元素

当然, 苹果工程师也无法预测到所有的情况, 比方我们想要取得第二个元素. 这时候, 就需要进行DIY了

通过下标直接访问

WX20180614-220150@2x.pngWX20180614-220200@2x.png

显然, 不可以通过这两种方式来进行获取, 由于我们之前说到, Index这个占位符并不肯定是Int, 而是一个遵守了Comparable的类型.

WX20180614-220223@2x.png

切片

那么, 对于上面的例子来说, 我们有没有更加易于维护或者者说更加优雅的实现方式呢?

如果我们去掉首元素, 而后再获取新得到的集合的第一个元素, 那么, 即可以优雅的实现了.

WX20180614-221939@2x.png

那么, dropFirst所产生的对象, 就是一个切片, 在WWDC中, 将它比喻成了一个buffer.

注意内存

值得注意的是, 持有切片, 将使得即使将原来的集合置空, 内存也不会释放.
这里, 我的了解是这样的, 切片是一个 原有集合 + 映射关系 的产物. 所以, 除非将切片也置空, 否则, 原有集合并不会被释放.

共享索引

WX20180614-221957@2x.png

推迟计算

WX20180614-222457@2x.png

经过这样的一套操作, 我们计算了4004个元素, 假如我们后面还有少量其余的操作, 更糟糕的是, 假如我们最终只是取取first, 这样, 前面生成的那些元素, 都成为了白费.

这时, 我们能通过lazy关键字, 能规避这样的白费

WX20180614-223243@2x.png

能看到, 用lazy后, 刚才的遍历过程, 变成了组织一个新集合的过程,

WX20180614-223222@2x.png

只有在first进行计算的时候, 才进行计算

让我们通过一个更直观的例子来体验

验证

import UIKitclass ViewController: UIViewController {    var arr = Array<Int>()    var result: Int?        override func viewDidAppear(_ animated: Bool) {        super.viewDidAppear(animated)                for i in 0..<10000000 {            arr.append(i)        }    }    @IBAction func slice(_ sender: UIBarButtonItem) {                result = arr.map { $0 * 2 }.map { $0 + 5 }.filter { $0 < 100 }.first        print(result)    }}

在这个例子中, 为了验证非lazy情况下白费的资源包含哪些, 以及上文中提到的 切片是一个 原有集合 + 映射关系 的产物 的结论验证

我用的模拟器进行测试, 在初始内存为125M, 当我点击按钮, 开始进行计算的时候, 能看到CPU和内存都有飙升, 内存波动飙升到274M后, 下降到125M. CPU则飙升到100%后, 下降到0.

而用lazy后, 内存和CPU几乎没有变化

用情况

  • 链式计算
  • 仅仅需要求值结果中的某一部分
  • 本身的计算不影响外部

multi & safe

可变集合

用了失效索引

WX20180614-224232@2x.pngWX20180614-224242@2x.png

复使用写之前的索引

WX20180614-224440@2x.pngWX20180614-224451@2x.png

如何规避

  • 在持有索引和切片时, 解决要谨慎
  • 集合发生改变时, 要升级索引后再用
  • 在需要索引和切片的情况下才对其进行计算

多线程访问

WX20180614-224839@2x.png

如何规避

  • 用单线程进行访问
  • 用Thread Sanitizer

其余建议

假如能, 尽量用带capacity的初始化函数去初始化你的集合, 由于这样节省少量不必要的内存开销, 尽管这并不可以节省多少, 但是想象你的项目中有成千上万个集合对象, 他们能省出一个相当可观的内存数量.

桥接

Foundation Collection

WX20180614-225245@2x.png

值类型与引使用类型

在swift 中的集合, 都是值类型, 为什么这么设计呢? 让我们先看一组图片

WX20180614-225539@2x.pngWX20180614-225609@2x.png

引使用类型的操作

  1. 我们有一个集合x
  2. 当我们执行 let y = x 的时候, y指针会指向x所指向的内存空间
  3. 当我们继续执行append的时候, x和y所指的集合新添加一个元素

值类型的操作

  1. 我们有一个集合x
  2. 当我们执行 let y = x 的时候, y指针会指向x所指向的内存空间
  3. 当我们继续执行append的时候, y所指向的集合将x内容拷贝进集合, 并将新元素放入集合

对于值类型来说, 这样有什么好处呢? 由于在现代CPU在设计的时候, 采使用了缓存机制, 能快速的访问连续区域的地址. 而值类型的这种操作, 各个元素之间的内存是相连的, 而引使用类型的则不是.

Swift与Foundation Collection的桥接

桥接就是把一种语言的某个类型转换为另一种语言的某个类型. 桥接在swift与OC之间是双向的, 也是必要的, 当然, 也是有少量资源开销的, 能通过Instrument进行测量

WX20180614-231112@2x.pngWX20180614-231133@2x.png

这里的桥接发生在

  • NSMutableAttributedString取string上, return bridge.
  • 需要传入一个NSString, 的参数类型桥接 param bridge

WX20180614-231207@2x.png

尽管在这里也发生了桥接, 但是集合能忽略不计

建议

建议在开发过程中, 尽量避免Swift的collection与NS以及CF的collection进行混使用. 由此, 笔者猜测, swift中的类型去掉NS头的起因, 就是为了方便辨认能否需要桥接, 当然, 假如确切的知道能否存在桥接的损耗, 还是需要通过Instrument进行测量.

最后

WX20180614-232351@2x.png

说明
1. 本站所有资源来源于用户上传和网络,如有侵权请邮件联系站长!
2. 分享目的仅供大家学习和交流,您必须在下载后24小时内删除!
3. 不得使用于非法商业用途,不得违反国家法律。否则后果自负!
4. 本站提供的源码、模板、插件等等其他资源,都不包含技术服务请大家谅解!
5. 如有链接无法下载、失效或广告,请联系管理员处理!
6. 本站资源售价只是摆设,本站源码仅提供给会员学习使用!
7. 如遇到加密压缩包,请使用360解压,如遇到无法解压的请联系管理员
开心源码网 » [ WWDC2018 ] – 高效用集合 Using Collections Effectively

发表回复