(WWDC) Swift 更高效地使用集合(Using Collections Effectively)


目录

  • 基本原理
  • 索引和切片
  • 懒加载
  • 修改和多线程
  • Foundation框架和桥接(Bridging)



基本原理



Collection协议

protocol Collection : Sequence {
    associatedtype Element
    associatedtype Index : Comparable

    subscript(position: Index) -> Element { get }

    var startIndex: Index { get }
    var endIndex: Index { get }

    func index(after i: Index) -> Index
 }



startIndex, endIndex, index(after: )以及subscript下标的使用示例:

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 }
            iter = index(after: next)
        }
    }
}
(1...10).everyOther { print($0) }


各种集合协议之间的关系

1306450-24c810a63b292c49.png
Collections Protocol Hierarchy




索引和切片



首先介绍这两种集合协议

Bidirectional Collections:
func index(before: Self.Index) -> Self.Index

Random Access Collections:
// constant time
func index(_ idx: Index, offsetBy n: Int) -> Index
func distance(from start: Index, to end: Index) -> Int


Swift中常见的集合类型

Array, Set, Dictionary



以获取集合中的第二个元素为例,讲解索引和切片的用法:

// 多次判定,然后通过索引获取第二个元素
extension Collection { 
    var second: Element? {
        // Is the collection empty?
        guard self.startIndex != self.endIndex else { return nil } 
        // Get the second index
        let index = self.index(after: self.startIndex)
        // Is that index valid?
        guard index != self.endIndex else { return nil } 
        // Return the second element
        return self[index]
    } 
}

使用切片更高效地获取第二个元素:

// 使用dropFirst获取去除第一个元素之后的切片,然后获取切片的第一个元素
var second: Element? {
    return self.dropFirst().first
}



切片会引用原始的集合,如果切片不释放引用,原始的集合依然存在于内存中:

extension Array {
    var firstHalf: ArraySlice<Element> {}
    return self.dropLast(self.count / 2)
}
var array = [1, 2, 3, 4, 5, 6, 7, 8]
var firstHalf = array.firstHalf // [1, 2, 3, 4]
array = []

print(firstHalf.first!) // 1

// 复制切片到另一个集合中,释放原始的集合
let copy = Array(firstHalf) // [1, 2, 3, 4]
firstHalf = []
print(copy.first!)




懒加载(lazy方法)



避免低效地去迭代集合中的所有值来获取部分值:

// 返回值为[Int]类型;
// 迭代所有的值,获取最后结果;
let items = (1...4000).map { $0 * 2 }.filter { $0 < 10 }
print(items.first) // 迭代1...4000后获取第一个值

// 返回值为LazyFilterCollection<LazyMapCollection<(ClosedRange<Int>), Int>>类型;
// 在需要获取部分值时,可以避免迭代所有的值;
// 但是每次从items获取值,都会进行新一次的计算;
let items = (1...4000).lazy.map { $0 * 2 }.filter { $0 < 10 } 
print(items.first) // 仅迭代一次



懒加载会重复计算,可以将结果保存到另一个集合中:

let bears = ["Grizzly", "Panda", "Spectacled", "Gummy Bears", "Chicago"]
let redundantBears = bears.lazy.filter {
    print("Checking '\($0)'")
    return $0.contains("Bear")
}

// 输出"Grizzly", "Panda", "Spectacled", "Gummy Bears"这三个元素
// 每次调用redundantBears.first都会输出这3个元素
print(redundantBears.first!)

// 将结果赋值给另一个集合,以防止每次获取过滤结果都进行计算
let filteredBears = Array(redundantBears)
print(filteredBears.first!)


何时使用lazy方法?
  • 链式计算
  • 只需要集合中的一小部分数据
  • 没有副作用
  • 避免API边界 (在返回结果给API的调用方时,返回一个实际的集合而不是懒加载对象)




修改集合



接下来介绍另外两种集合协议:

Mutable Collection
// constant time
subscript(_: Self.Index) -> Element { get set }

Range Replaceable Collections
replaceSubrange(_:, with:)




访问集合时发生了崩溃?思考以下几个问题:

  • 是否修改了集合?
  • 是否是多线程操作集合?



在数组中使用无效的索引,会发生崩溃:

var array = ["A", "B", "C", "D", "E"] 
let index = array.firstIndex(of: "E")! 
array.remove(at: array.startIndex) 

// index已失效
print(array[index]) // Fatal Error: Index out of range.

// 重新获取有效的索引可以避免这种问题发生
if let idx = array.firstIndex(of: "E") {
    print(array[idx]) 
}

在字典中依然要面对这种问题:

var favorites: [String : String] = [
    "dessert" : "honey ice cream",
    "sleep" : "hibernation",
    "food" : "salmon"
]

let foodIndex = favorites.index(forKey: "food")!
print(favorites[foodIndex]) // (key: "food", value: "salmon")

favorites["accessory"] = "tie"
favorites["hobby"] = "stealing picnic supplies"

print(favorites[foodIndex]) // Fatal error: Attempting to access Dictionary elements using an invalid Index

// 重新获取有效的索引可以避免这种问题发生
 if let foodIndex = favorites.index(forKey: "food") {
     print(favorites[foodIndex])
 }



使用索引和切片的建议:

  • 谨慎使用索引和切片;
  • 修改会导致失效;
  • 只在需要时才去进行获取;




你的集合可以多线程访问吗?



运行以下代码,会发生什么事?

var sleepingBears = [String]()
let queue = DispatchQueue.global() // 并发队列,会有多个线程
queue.async { sleepingBears.append("Grandpa") }
queue.async { sleepingBears.append("Cub") }

sleepingBears的值可能为 ["Grandpa", "Cub"], ["Cub", "Grandpa"], ["Grandpa"], ["Cub"]
甚至出现类似的错误:malloc: *** error for object 0x100586238: pointer being freed was not allocated

使用ThreadSanitizer(TSAN)进行检测,可以看到类似如下信息:

 
WARNING: ThreadSanitizer: Swift access race
Modifying access of Swift variable at 0x7b0800023cf0 by thread Thread 3:
...
 Previous modifying access of Swift variable at 0x7b0800023cf0 by thread Thread 2:
 ...
 Location is heap block of size 24 at 0x7b0800023ce0 allocated by main thread:
 ...
SUMMARY: ThreadSanitizer: Swift access race main.swift:515 in closure #1 in gotoSleep()



使用串行队列替换global()并发队列,即可使集合的操作在单个线程中进行。

var sleepingBears = [String]()
let queue = DispatchQueue(label: "Bear-Cave")  // 串行队列,单一线程调度
queue.async { sleepingBears.append("Grandpa") }
queue.async { sleepingBears.append("Cub") }
queue.async { print(sleepingBears) } //  ["Grandpa", "Cub"]



多线程操作集合的建议:

  • 尽可能从单一线程进行操作;
  • 如果必须进行多线程操作:
    • 1.使用互斥(加锁);
    • 2.使用TSAN(ThreadSanitizer)来检测错误;



建议使用不可变的集合:

  • 更容易获取不变的数据;
  • 缩小BUG的区域;
  • 用切片和懒加载来模拟修改;
  • 编译器会帮助你;



在使用集合时,提前预留空间,减少内存分配操作:
Array.reserveCapacity(_:)
Set(minimumCapacity:)
Dictionary(minimumCapacity:)



桥接(Bridging)



Foundation框架中的集合

Foundation框架中的集合都是引用类型,而Swift的集合都是值类型;
在操作Foundation框架中的集合时,要谨慎!

转换Objc中的类型为Swift类型时,桥接都会有消耗,虽然看似代价不大,但是如果桥接的量很大,这个过程就很值得关注了。



识别桥接问题:

  • 用Instruments可以帮助检测桥接的代价;
  • 在循环中,检测桥接的代价;
  • 找出以下常见问题:
    • _unconditionallyBridgeFromObjectiveC;
    • bridgeEverything;



观察以下代码:

let story = NSString(string: """
Once upon time there lived a family of Brown Bears. They had long brown hair.
...
They were happy with their new hair cuts. The end.
""")

let text = NSMutableAttributedString(string: story)

// text.string由NSString转为String,用Instruments检测到这个操作需要耗费大量时间
let range = text.string.range(of: "Brown")!  // Range<String.Index>
let nsrange = NSRange(range, in: text.string) // NSRange
text.addAttribute(.foregroundColor, value: NSColor.brown, range: nsrange)



桥接操作发生在哪里?

1306450-ce6eeaf6ca2591bb.png
Bridging



更改耗时严重的桥接操作代码,优化性能:

let string = text.string as NSString // NSString
let nsrange = string.range(of: "Brown") // 依然有桥接操作发生,"Brown"由String转为NSString



何时使用Foundation框架中的集合?

  • 需要使用引用类型;
  • 操作已知的代用品:如: NSAttributedString.string、Core Data中的Managed Objects;
  • 你已经识别并且测试了桥接的代价;




接下来怎么办?

  • 查看你已经使用的集合;
  • 测试你的集合相关代码 (TSAN, Instruments);
  • 审查集合的修改状态;
  • 在Playground中操练,然后掌握这些知识要点;




如有错误,欢迎指出!?






参考内容:
Using Collections Effectively - Apple WWDC




转载请注明出处,谢谢~

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值