是什么使代码 “Swifty”? —— Fast

2023-09-21 15:01:23 浏览数 (2)

Swift的官方网站上的About页面列出了三个关键字:

  • 安全(Safe):为了最大限度地减少开发人员的错误;
  • 迅速(Fast):执行的速度要快;
  • 表现力(Expressive):因为Swift的目标是尽可能清晰易懂。

是什么使代码 “Swifty”? —— Safe 介绍了如何有选择地使用类型系统的各个方面和功能,以使我们的代码更易于理解和使用。

是什么使代码 “Swifty”? —— Expressive 介绍了如何使用表达性命名和API设计传达我们的代码意图

Swifty Code —— Fast

性能之路(The path to performance)

Swift的第二个核心目标,就是要快一些,总的来说,这有点棘手。毕竟,编写高性能代码的主要部分在于测量,微调和再次测量。但是,使我们的代码在性能方面与Swift本身更加一致的一种方法是,充分利用标准库所提供的功能——特别是在处理集合(例如字符串)时。

就像我们在 Swift:字符串解析和Swift:集合切片中看过一样,Swift标准库针对性能进行了高度优化,并且使我们能够以高效的方式执行许多常见的集合操作-假设我们使用正确的API。

例如,从字符串中删除一组特定字符的一种常见方法是使用旧的ReplacementOccurences(of:with :)API,该API是Swift的String类型从其表亲Objective-C的NSString继承而来的。在这里,我们使用了对该API的一系列调用,以通过删除一组特殊字符来清理字符串:

代码语言:javascript复制
let sanitizedString = string
    .replacingOccurrences(of: "@", with: "")
    .replacingOccurrences(of: "#", with: "")
    .replacingOccurrences(of: "<", with: "")
    .replacingOccurrences(of: ">", with: "")

上面的实现的问题是,它将导致我们的字符串进行4次单独的迭代——使用较短的字符串,或者在不经常遇到的代码路径中进行上述操作时,这可能不是问题,但可能会变成当我们需要最大性能时的瓶颈。

值得庆幸的是,Swift通常不需要我们在性能代码和优雅代码之间进行选择,我们要做的就是切换到一种更合适的API,在Set中这个API仅通过我们的字符串一次即可删除其中包含的每个字符。,像这样:

代码语言:javascript复制
let charactersToRemove: Set<Character> = ["@", "#", "<", ">"]
string.removeAll(where: charactersToRemove.contains)

因此,从性能的角度来看,使我们的代码更“Swifty”,有时我们要做的就是探索标准库在面对给定任务时必须提供的内容,尤其是在集合,机会方面相当高,因为有一个优雅,简单的API,它还为我们提供了出色的性能特征。

文章来自 John Sundell的What makes code “Swifty”?中关于Fast的内容

附几个简单性能优化例子:
  • 在这篇文章也是用到了文中这个方法iOS - DeviceToken 解析来解析Token
  • swift filter会创建全新的数组,且会对所有元素进行操作,例如:
代码语言:javascript复制
bigArray.filter { someCondition }.count > 0

写成如下形式性能更好:

代码语言:javascript复制
bigArray.contains { someCondition }

这种做法会比原来快得多,主要因为两个方面:它不会去为了计数而创建一整个全新的数组, 并且一旦找到了第一个匹配的元素,它就将提前退出。一般来说,你只应该在需要所有结果时才去选择使用 filter

  • swift Stringprefix总是从头开始,例如:
代码语言:javascript复制
extension String {
var allPrefixes1: [Substring] {
return (0...self.count).map(self.prefix) }
}
let hello = "Hello"
hello.allPrefixes1 // ["", "H", "He", "Hel", "Hell", "Hello"]

这段代码看上去简单,但是它非常低效。首先,它会遍历一次字符串,来计算其⻓度,这没什 么大问题。但是,之后 n 1 次对 prefix 的调用中,每一次都是一个 O(n) 操作,这是因为 prifix总是要从头开始工作,然后在字符串上经过所需要的字符个数。在一个线性复杂度的处 理中运行另一个线性复杂度的操作,意味着算法复杂度将会是 O(n2)。随着字符串⻓度的增⻓, 这个算法所花费的时间将以平方的方式增加。

如果可能的话,一个高效的字符串算法应该只对字符串进行一次遍历,而且它应该操作字符串 的索引,用索引来表示感兴趣的子字符串。这里是相同算法的另一个版本:

代码语言:javascript复制
extension String {
var allPrefixes2: [Substring] {
return [""]   self.indices.map { index in self[...index] } }
}
let hello = "Hello"
hello.allPrefixes2 // ["", "H", "He", "Hel", "Hell", "Hello"]

上面的代码依然需要迭代一次字符串,以获取索引的集合 indices。不过,一旦这个过程完成, map 中的下标操作就是 O(1) 复杂度的。这使得整个算法的复杂度得以保持在 O(n)。

例子来自《Swift进阶》一书原作者【德】Chris Eidhof(克里斯·安道夫) 【德】Ole Begemann (奥勒·毕格曼) 【德】Airspeed Velocity (空速网站),中文版由王巍译

是什么使代码 “Swifty”? —— Safe 介绍了如何有选择地使用类型系统的各个方面和功能,以使我们的代码更易于理解和使用。

是什么使代码 “Swifty”? —— Expressive 介绍了如何使用表达性命名和API设计传达我们的代码意图

0 人点赞