[ WWDC2018 ] - 高效使用集合 Using Collections Effectively

一个没有集合的世界

假如世界没有集合

假如没有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 bear
print("\(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.png
WX20180614-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 UIKit

class 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.png
WX20180614-224242@2x.png

复用写之前的索引


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

如何规避

  • 在持有索引和切片时, 处理要谨慎
  • 集合发生改变时, 要更新索引后再使用
  • 在需要索引和切片的情况下才对其进行计算

多线程访问

WX20180614-224839@2x.png

如何规避

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

其他建议

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

桥接

Foundation Collection

WX20180614-225245@2x.png

值类型与引用类型

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

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

推荐阅读更多精彩内容

  • 在一个方法内部定义的变量都存储在栈中,当这个函数运行结束后,其对应的栈就会被回收,此时,在其方法体中定义的变量将不...
    Y了个J阅读 4,410评论 1 14
  • 一、集合入门总结 集合框架: Java中的集合框架大类可分为Collection和Map;两者的区别: 1、Col...
    程序员欧阳阅读 11,519评论 2 61
  • Swift1> Swift和OC的区别1.1> Swift没有地址/指针的概念1.2> 泛型1.3> 类型严谨 对...
    cosWriter阅读 11,076评论 1 32
  • 其实我不知道自己想写些什么,或许只是我的心血来潮,也或许是因为我太过无聊,突然就想写些东西,那么,我便来写写关于我...
    陌槿璃墨墨阅读 184评论 0 1
  • 1. 看到反对者,第一让我想到了我的客户,由于每天都会接触到不同层次的客户,每天都会给他们做各种的风险规划方案,不...
    Sherry001阅读 95评论 0 0