Session 连接git
做为一个 iOS App 开发人员, 常常会听到这样的吐槽, 作一个 App 主要是 UI 布局和动画, 平时基本上都用不到算法, 为啥面试的时候总喜欢考算法?程序员
本身也有过这样的疑惑, 项目中确实不多使用到算法, 通常就是经常使用的几种设计模式用熟, MVC 和 MVVM 选一个, 而后就开始各类第三方库, 难一点的可能会遇到一些多线程的问题或者组件化开发?github
放出 PPT 里面的一张图感觉下, 确实很好的总结了 iOS App 开发的精髓.面试
可是看过这个 Session 以后, 算是获得一些启示, 一个程序员的自我修养最终仍是绕不过算法, 代码的优雅和高效始终是咱们所追求的, 这无关乎业务和面试.算法
以为有必要介绍下演讲者swift
WWDC 2015 时候第一次看到 Dave Abrahams 的演讲, 当时讲的是这个 Session "Protocol-Oriented Programming in Swift".设计模式
在维基百科上搜了下, Dave Abrahams 以前就已是 C++ STL 的贡献者之一了, 13年加入 Apple 在 Swift 的核心库小组里面担任 TL. 因此这篇演讲也引用了一些 C++ STL 中的哲学思想.数组
Dave Abrahams 的演讲方式也挺有意思的, 采用一种自编自导自演的方式, 创造了一个苛刻的老学究的角色, 模拟对话, 而后引出演讲的主题. 将本该严谨死板的算法讲出了一些趣味和发人深省的地方.多线程
除了 Session 视频, PPT 固然也是要下载的, 得益于 Swift 的开源, PPT 中的代码实现均可以在 GitHub 上找到.架构
GitHub地址: https://github.com/apple/swift, 固然 master 分支上面是没有的, 切到 swift-4.2-branch-06-11-2018 分支而后在 swift/stdlib/public/core/RangeReplaceableCollection.swift 文件里面能够找到 removeAll 方法, 里面就是 PPT 中讲到的实现.
可是比较奇怪的是在 swift-4.2-branch 分支上面这个实现已经变了, 估计 Apple 的开发人员一直在优化这块的实现, 毕竟4.2目前还不是稳定版本.
Session 以一个图形 App 做为例子, 看一下这个 App:
而后引出一个 bug, 这个 bug 也是咱们新手开发 App 的时候比较容易犯错的一个问题, 对于数组边遍历边删除的问题.
看一下问题:
如图, 图中有10个图形, 其中咱们选中第8个将其删除, 可是删除的时候 crash 了, why?
看一下问题代码:
extension Canvas {
mutating func deleteSelection() {
for i in 0..<shapes.count {
if shapes[i].isSelected {
shapes.remove(at: i)
}
}
}
}
复制代码
遍历的范围 0..<shapes.count
一开始就已经肯定了(10个元素), 当遍历到第8个图形的时候, 发现其被选中则进行 remove 操做, 后面两个元素往前补位, 这个时候数组里面只有9个元素了, 因此再按照最开始的范围遍历到第十个元素时组数越界产生 crash.
由于平时使用 Objective-C 比较多, 咱们结合 Objective-C 来看看, 咱们熟悉的数组遍历方式有:
for (NSInteger i = 0; i < shapes.count; i++) {
// do something
}
复制代码
for (Shape *shape in shapes) {
// do something
}
复制代码
[shapes enumerateObjectsUsingBlock:^(Shape * _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {
// do something
}];
复制代码
以上除了第二种方式会抛异常之外, 1和3这两种都能"混"过去, 为何是"混", 咱们来分析下, 假设这个 bug 中的第9个元素也是被选中的, 那么当遍历到第8个图形的时候, 发现其被选中则进行 remove 操做, 后面两个元素往前补位, 可是此时下标并无处理, 下一次遍历会直接从第9个元素开始(也就是原先的第10个元素), 从而把原生的第9个元素直接跳过去了, 出现了漏删除的行为.
此类问题我出过一个面试题, 面试题不是很难, 有近一半的面试者出现过边遍历边删除的问题(为啥出这个题, 由于我也是踩过坑的~).
好了回到正题上, 那么缘由找到了, 具体怎么个解法呢? Session 中还绕了好几个弯, 咱们先来看第一个弯:
extension Canvas {
mutating func deleteSelection() {
var i = 0
while i < shapes.count {
if shapes[i].isSelected {
shapes.remove(at: i)
}
i += 1
}
}
}
复制代码
这个改法和普通的 for 循环相似, 只是改为了 while 循环, 问题也比较明显, 假设若是第9个元素也一样被选中, 就会存在漏删的问题, 缘由上面已经分析过了.
既然是由于下标没有处理, 那么处理下下标不就能够了? 第二个弯:
extension Canvas {
mutating func deleteSelection() {
var i = 0
while i < shapes.count {
if shapes[i].isSelected {
shapes.remove(at: i)
}
else {
i += 1
}
}
}
}
复制代码
这个解法是可行的, 还有别的解法么? 逆向思惟下, 既然删除一个元素以后, 后面的元素是往前进行补位的, 这样影响的是正序遍历时候的下标. 若是咱们采用逆序遍历是否是就不存在这个问题了? 第三个弯:
extension Canvas {
mutating func deleteSelection() {
for i in (0..<shapes.count).reversed() {
if shapes[i].isSelected {
shapes.remove(at: i)
}
}
}
}
复制代码
其实咱们通常修改 bug 的话至此就已经完事了, 甚至连逆向思考一下可能都不会去想, 其实这只是刚刚开始.
鼠标移到 remove 方法, 按住 option 键而后点击查看下文档, remove 方法竟然是个 O(n) 复杂度的操做. 再加上外层的 while 循环, 整个方法的复杂度有O(n²), 看到这里我也吃了一惊.
后面, 做者给咱们科普了下算法的复杂度还有 Mac 上字典中对于算法的定义. 应该也是做为一个引子吧.
这个时候已经不是在解 bug 了, 上升了一个层次 - 代码优化, 先放代码:
extension Canvas {
mutating func deleteSelection() {
shapes.removeAll(where: { $0.isSelected })
}
}
复制代码
代码精简了不少, 语义也十分清晰, 这里多了个 removeAll 方法, 这个方法应该是 Swift 4.2 新的方法, 以前的版本并无找到这个方法. 固然整个过程是值得咱们学习的, 对于咱们后续封装本身的扩展方法也是颇有启发的.
若是你装了 Xcode 10 能够点开 removeAll 的文档看一下, 复杂度为 O(n), 这里是否是勾起了你的好奇心, 从 O(n²) -> O(n) 这个是怎么办到的? 若是是你本身优化了这个解法, 是否是这一成天都是神清气爽的.
extension RangeReplaceableCollection where Self: MutableCollection {
/// Removes all the elements that satisfy the given predicate.
///
/// Use this method to remove every element in a collection that meets
/// particular criteria. This example removes all the odd values from an
/// array of numbers:
///
/// var numbers = [5, 6, 7, 8, 9, 10, 11]
/// numbers.removeAll(where: { $0 % 2 == 1 })
/// // numbers == [6, 8, 10]
///
/// - Parameter predicate: A closure that takes an element of the
/// sequence as its argument and returns a Boolean value indicating
/// whether the element should be removed from the collection.
///
/// - Complexity: O(*n*), where *n* is the length of the collection.
@inlinable
public mutating func removeAll( where predicate: (Element) throws -> Bool
) rethrows {
if var i = try firstIndex(where: predicate) {
var j = index(after: i)
while j != endIndex {
if try !predicate(self[j]) {
swapAt(i, j)
formIndex(after: &i)
}
formIndex(after: &j)
}
removeSubrange(i...)
}
}
}
复制代码
上半部分就先讲到这, 下半部分还会用到这个算法, 到时候详细阐述下.
最后放上一句 PPT 中的至理箴言 "No Raw Loops". 怎么作到这一点? 那就是对 Swift 标准库要作到如数家珍.