Swift中的字符串

96 阅读2分钟

Swift中的字符串

hudson译 原文

编程语言如何建模文本通常比最初看起来要复杂得多——Swift的String类型也不例外。虽然字符串是任何程序都会处理的最常见的数据之一,也是人们非常熟悉的东西,但当涉及到如何在代码中表示文本时,存在一些非常现实的挑战。了解其中一些挑战,以及Swift如何克服这些挑战,通常是在不同环境中更轻松地处理字符串的关键。

Swift字符串被建模为使用非常常见的UTF-8文本编码进行编码的字符集合,这使得它们可以表示各种不同的字符和表情符号。由于字符串是集合,它们既可以使用字面量(在代码中内联定义的字符串)初始化,也可以使用另一个包含Character元素的集合——例如数组:

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)
    }
}

甚至可以直接在字符串上使用最常见的集合操作,如mapflatMapcompactMap。例如,以下代码在字符串上使用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 // 4Café“.utf8.count // 5

正如上面所看到的,字符串中感知到的字符数和UTF-8字符的实际数并不总是相等的。当我们开始在组合中添加表情符号时,这种差异会更大——特别是那些由许多不同代码单元组成的表情符号,例如家庭表情符号的不同变体:

”👨‍👩‍👧‍👦“.count // 1
”👨‍👩‍👧‍👦“.utf8.count // 25

由于上述字符在幕后实际工作方式的差异,Swift没有提供使用原始Int索引访问字符串字符的方法——而是选择一种更固定和安全的方法,使用专用的String.Index类型和特定的API来操作此类索引。每个字符串都带有一个startIndexendIndex,使用它们,可以推导出希望检索字符的任何其他索引——像这样:

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]) // !

我们还可以从字符串索引中形成范围,并使用这些范围提取字符串的一部分——通常称为子字符串:

let range = secondIndex..<lastIndex
let substring = string[range]
print(substring) // ello, world

Swift中子字符串的有趣之处在于,它们实际上不是String值。相反,它们使用Substring类型表示,这使我们能够检索和传递子字符串,而无需不断复制底层字符串——这非常适合在处理大文本体的情况下的性能。然而,字符串和子字符串不是相同类型的实例,这可能会导致一些棘手的情况,例如如果尝试将子字符串赋值给UILabeltext属性:

let label = UILabel()
label.text = substring // Compiler error

会得到一个编译器错误,说Substring不能分配给String?属性,因为就编译器而言,它们是完全独立的类型。谢天谢地,将子字符串转换为适当的字符串就像这样做一样简单:

label.text = String(substring)

在执行上述转换时,需要记住的一件事是,它确实将子字符串复制到一个新的字符串中,这通常是我们想要的,因为它还允许底层字符串从内存中释放(如果没有其他子字符串仍然引用它)。

虽然StringSubstring是不同的类型,但它们确实有很多共同的API——因为它们都符合相同的StringProtocol。当我们想编写可用于字符串或子字符串的范型代码时,StringProtocol非常方便——例如从字符串中提取所有字母的函数:

func letters<S: StringProtocol>(in string: S) -> [Character] {
    return string.filter { $0.isLetter }
}

除了isLetter,Swift的Character类型还附带了一整套其他属性,可以轻松检查我们正在处理哪种字符。

与其他编程语言相比,Swift的字符串建模方式起初可能看起来相当复杂,但Swift的String API的设计方式是有充分理由的——特别是当查看字符串的实际存储方式时,以及考虑到在现代国际应用程序中准确表示文本的一些挑战时。总体而言,Swift字符串提供了出色的性能和安全性,尽管它可能会在这里或那里有那么一些不便利。

感谢您的阅读!🚀