为什么使用isEmpty会比使用count == 0更快

5,051 阅读5分钟

原文:Why using isEmpty is faster than checking count == 0

如果你想要检查一个数组,集合, 字符串或者是其他的集合类型是空,你可能会写如下代码:

let name = ""
if name.count == 0 {
	print("You're anonymous!")
}

然后,这段代码可以像这样被更好的表达:

if name.isEmpty {
	print("You're anonymous!")
}

使用isEmpty能更加清晰的去读,运行也更快。

Swift的字符串是如何编译的

为了了解对于字符串而言,为什么isEmpty比count == 0更快,我们需要研究一下Swift中的字符串是如何工作的。

Swift的字符串是一种复杂的字符集合,多个符号可能会组合成一个字符给用户看。举个例子,英国国旗这个表情是由两个不同的字符组成的: "G"和"B"。当然不是那些文字字符,而是当Unicode符号并排放置的时候,它们就会自动变成英国国旗。

你可以看看下面截图中的操作: 它将区域指示符号G存储在一个字符串中,区域指示符号B存储在在第二个字符串中,然后将它们合并在一起组成第三个字符串。就像你看到的这样,它们分开看是被虚线环绕的G和B,但是合在一起,它们就自动变成了英国国旗。

看看这些字符串的count值,三个都是1。因此,从原生字符的方面来看,它是两个字符,但是从用户方面来看,它就是一个字符: 一面英国国旗。他们不想意外的将旗帜分成两半。Swift就是被设计成阻止我们将Unicode字符串意外打破,所以它会认为emoji就是一个字符。

让这个事情更具迷惑性,在这种场景背后,每个特殊字符G和B可以用UTF-16表示成两个值,或者用UTF-8表示成四个值。

字符串中的索引

由于这种复杂性,它不可以通过整数,在一个字符串中单一读取字符,这也意味着下面这种代码是不能被编译的:

 let name = "Guybrush Threepwood"
 print(name[0])

那不是因为这样的代码是不能工作的。事实上,我们可以为字符串添加下面的扩展:

extension String {
    subscript(i: Int) -> Character {
        return self[index(startIndex, offsetBy: i)]
    }
}

然而,事情不是这么简单的。因为每个字符串中的字符,可能存储着一个值,两个值,四个值,或者可能是12个值。Swift不能提前知道第五个字符是什么 - 我们的扩展起始于第一个字符,由i个字符计算,直到找到我们要求的字符。

这会成为问题,因为你可能会尝试写下面的这段代码,去输出所有字符串中的字符。

for i in stride(from: 0, to: name.count, by: 2) {
    print(name[i])
}

我们说这个循环的时间复杂度是O(n)。所以,一个单一字符的字符串可能花费一秒钟去遍历,一个两个字符的字符串可能花费2秒,一个三个字符的字符串可能花费3秒,然后以此类推 - 它是线性相关的。

然而我们的字符串的下标扩展,也是O(n)复杂度的。结果就是,我们现在得到了一个O(n)的操作中又有一个O(n),然后就变成了O(n^2)的操作。使用上面的代码,就意味着一个2个字符的字符串,需要花费4秒,一个3个字符的字符串,将会花费9秒,一个10个字符的字符串,将会花费100秒,以此类推。

所以,即使我们的代码看起来将会执行的相对较快,实际上可能会非常的慢。

回到isEmpty

既然你明白了Swift的字符串内部是如何工作的,那就让我们回到isEmpty vs count == 0。 就像你看到的,Swift隐藏了所有的复杂性: 一个用户可见的字符可能是一打底层的值组合而成的。所以我们使用count去读取字符串的长度的时候,Swift需要去遍历所有的字符,以确定这个字符串有多长。它从头开始,一直计数到结束。

这就意味着,读取一个字符串的count,是一个O(n)的操作: 如果你有一个空的字符串,它基本就是瞬时的,但是如果你有完整的莎士比亚作品集的话,它就会话费一些时间。

作为对比,使用isEmpty在做完一个比较简单的比较后,就能返回true或者false: 我们的字符串的开始的索引是否等于结束索引?如果相等的话,那么字符串就是空的,然后我们就完成了。它不需要计算所有的字符。

现在这个特殊的问题不适用于数组,集合,字典,只适用于字符串,所以你可以在其他地方使用count == 0,而只在字符串中使用isEmpty。然而出于以下两个原因,我会谨慎对待这个选择:

1. isEmpty是一直都比检查特定的值来的清晰,通常用代码表达你自己的意图,是成功的一半。
2. 你可以更加简单的避免在字符串中使用count == 0,因为你可以在你的代码中移除所有的实例。

幸运的是,SwiftLint和SwiftFormat可以为你关注这个,因为他们有选择性规则,可以准确的检测这种情况。