编程语言如何建模文本往往比它最初看起来要复杂得多——Swift的字符串类型也不例外。虽然字符串是任何程序都会处理的最常见的数据片段之一,也是我们作为人们非常熟悉的东西,但当涉及到如何在代码中表示文本时,有一些非常现实的挑战。理解其中的一些挑战,以及Swift是如何克服它们的,这通常是使在不同的环境下使用字符串变得更容易的关键。
Swift字符串被建模为使用非常常见的UTF-8文本编码的字符集合,这使得它们可以表示各种不同的字符和表情符号。 由于字符串是集合,它们既可以使用文字(代码中内联定义的字符串)初始化,也可以使用另一个包含字符元素的集合(例如数组)初始化:
let stringA = "Hello!"
let stringB = String(["H", "e", "l", "l", "o", "!"])
print(stringA == stringB) // true
字符串在许多其他方面的行为也类似于其他集合(如数组或字典)。例如,可以很容易地遍历字符串中的所有字符,就像遍历数组中的元素一样:
func printCharacters(in string: String) {
for character in string {
print(character)
}
}
我们甚至可以直接在字符串上使用最常见的集合操作,如map、flatMap和compactMap。 例如,下面是我们如何在一个字符串上使用map来将它转换成一个字符数组:
func characters(in string: String) -> [Character] {
return string.map { $0 }
}
因为字符串和数组在它们所支持的api类型方面如此相似,我们可能还希望能够使用基于int的下标从字符串中检索任何字符——像这样:
let string = "Hello, world!"
let character = string[1]
然而,运行上面的代码会给我们一个编译错误,因为(与数组不同)我们不能通过使用Int索引来随机访问字符串中的任何字符。为了理解这是为什么,我们必须深入到一个层次——超越String的表面层次API,并看看字符串实际上是如何在底层表示的。
让我们首先接受一个包含特殊(或非ascii)字符(本例中为café)的字符串,并比较它的字符数与用于表示它的UTF-8代码单元数之间的差异:
"Café".count // 4
"Café".utf8.count // 5
如上所述,字符串中可感知的字符数和实际的UTF-8字符数并不总是相等的。当我们开始把表情符号添加到组合中时,这种差异会变得更大——尤其是那些由许多不同代码单元组成的表情符号,比如家庭表情符号的不同变体:
"👨👩👧👦".count // 1
"👨👩👧👦".utf8.count // 25
由于上述字符在底层工作方式上的差异,Swift没有提供一种使用原始Int索引访问字符串字符的方法,而是选择了一种更加锁定和安全的方法,使用专用String.Index类型,以及用于操作这些索引的特定api。每个字符串都有一个startIndex和一个endIndex,使用它们可以派生出任何希望检索字符的其他索引——像这样:
let string = "Hello, world!"
let secondIndex = string.index(after: string.startIndex)
let thirdIndex = string.index(string.startIndex, offsetBy: 2)
let lastIndex = string.index(before: string.endIndex)
print(string[secondIndex]) // e
print(string[thirdIndex]) // l
print(string[lastIndex]) // !
我们也可以在字符串索引之外形成range,并使用这些range来提取字符串的一部分——通常称为子字符串:
let range = secondIndex..<lastIndex
let substring = string[range]
print(substring) // ello, world
Swift中子字符串的有趣之处在于它们实际上不是字符串值。相反,它们使用Substring类型表示,这使我们能够检索和传递子字符串,而无需不断复制底层字符串-当我们要处理大量文本时,这对性能很有帮助。然而,字符串和子字符串不是同一类型的实例,这可能会导致一些棘手的情况,例如,如果我们试图将子字符串分配为UILabel的文本:
let label = UILabel()
label.text = substring // Compiler error
上面的代码给了我们一个编译器错误,说一个Substring不能被赋值给一个String?属性,因为——就编译器而言——它们是完全不同的类型。值得庆幸的是,将子字符串转换为合适的字符串很容易:
label.text = String(substring)
在执行上述类型的转换时,需要记住的一点是,它确实将子字符串复制到一个新的字符串中,这通常是我们想要的,因为它还允许底层字符串从内存中释放(如果没有其他子字符串仍然引用它)。
虽然String和Substring是不同的类型,但它们有很多共同的api——因为它们都遵循相同的StringProtocol。当我们想要编写既可以用于字符串也可以用于子字符串的泛型代码时,这就非常方便了—例如下面这个函数从一个字符串中提取所有的字母:
func letters<S: StringProtocol>(in string: S) -> [Character] {
return string.filter { $0.isLetter }
}
除了isLetter, Swift的角色类型还附带了一整套其他属性,这使得我们可以很容易地检查我们所处理的角色类型。
与其他编程语言相比,Swift的字符串建模方法乍一看可能相当复杂,但Swift的字符串API是这样设计的有很好的理由-特别是当看到字符串实际上是如何存储的,以及在一个现代的,国际化的应用程序中准确表示文本的一些挑战。总的来说,Swift strings 给了我们很大的性能和安全性,尽管它可能会让我们在这里和那里损失一些方便。