第7章:字符串¶
在Swift
中正确实现字符串类型已经是一个有争议的话题了。该设计在Unicode
正确性、编码无关性、易用性和高性能之间取得了微妙的平衡。几乎每一个主要的Swift
版本都对String
类型进行了改进,使之成为我们今天的强大设计。要了解如何最有效地使用字符串,最好是了解它们到底是什么,它们如何工作以及如何表示。
在本章中,你将学习:
- 字符的二进制表示法,以及它是如何经过多年发展的
- 字符串的人类表示法
- 什么是字素群
Swift
如何使用UTF
编码,以及UTF的低级细节如何影响String
的性能- 字符串在不同地区的排序
- 什么是字符串折叠以及如何在字符串中进行最佳搜索
- 什么是子串以及它与内存的关系
- 自定义字符串插值,以及如何使用它从字符串初始化一个自定义对象或将其转换为一个字符串
二进制表示法¶
多年来,字符表示法发生了很大变化,从ASCII
(美国信息交换标准代码)开始,它用最多七个比特表示英文数字和字符。
然后,扩展ASCII
出现了,它使用了可由一个字节表示的其余128
个值。
但这对许多有不同字符集的语言来说并不适用。因此,另一个标准出现了,叫做ANSI
。这也是创建这个标准的实体的名字。美国国家标准协会。
与ASCII
不同,ANSI
不是一个单一的字符集。它实际上是多个字符集,每个字符集能够代表不同的字符。有希腊语(CP737
和CP869
)、希伯来语(CP862
)、土耳其语(CP857
)、阿拉伯语(CP720
)和其他许多字符集。每一个字符集的前127
个字符与ASCII
相同,但其余的字符集是ASCII-Extended
的一个变化。
这些字符集,在某种程度上解决了代表不同语言的不同字符的问题。但另一个问题出现了! 当你创建一个文件时,你需要用相同的字符集再次读取它。如果你使用不同的字符集,文件看起来就像一串随机的字符。只有当它是用正确的字符集打开的时候,对人来说才有意义。
例如,字节十六进制值0x9C
的字符,当用字符集CP-852
(又称Latin-2
)读取时,将显示字符ť
(小写t
加caron
)。但在字符集CP-850
,又称Latin-1
中,同样的字符将显示£
(磅符号)。你可以想象,一个打算用阿拉伯字符集阅读的文件,用西里尔字符集打开会是什么样子。
为了解决这个问题,Unicode
转换格式(UTF
)的出现,提供了一个代表所有字符的单一标准。然而,在这个UTF
标准之后,有四种不同的编码方式。UTF-7
、UTF-8
、UTF-16
和UTF-32
。每个数字代表该编码所使用的比特数。UTF-7
使用7
比特,UTF-32
使用32
比特(4
字节),等等。
需要知道的一个关键点是,UTF-8
、UTF-16
和UTF-32
都可以代表超过一百万个不同的字符。很明显,这组中的后者有很大的范围。至于第一种,它并不只限于8
位,它可以扩展到4
个字节以上。要涵盖UTF标准中所有可能的数值需要21
位。
UTF-8
的二进制表示法¶
UTF-8
的每个字符的大小从1字节到4字节不等。编码中保留了一些位,以确定这个字符从第一个字节开始使用多少个字节。
一个字节的最重要的位是0
值,它本身就是一个字符。该字符是1
个字节。
一个字节,其三个最重要的位有110
值,连同下面的字节,代表一个字符。该字符是2
个字节。
一个字节的四个最重要的位有1110
值,连同后面两个字节,代表一个字符。该字符是3
个字节。
一个字节的五个最重要的位具有11110
值,连同后面的三个字节,代表一个字符。该字符是4
个字节。
任何两个最重要的位具有10
值的字节都是属于字符的一部分(后面的字节)的字节。如果没有前面的字节,它本身就不能提供足够的信息。
对于UTF-8
来说,可用来存储数值的比特数计算如下。
1
个字节:8
位-1
个保留位=7
个可用位2
个字节:16
位-5
个保留位=11
个可用位3
个字节:24
位-8
个保留位=16
个可用位4
个字节:32
位-11
个保留位=21
个可用位
UTF-16
二进制表示法¶
UTF-16
是另一种可变长度的编码格式。一个字符可以是2
个字节或4
个字节。与UTF-8
类似,这种编码也有一个二进制表示法,以识别这2
个字节是整个字符还是还需要后面的2
个字节。
如果这2
个字节以0xD8
(二进制的110110
)开始,这两个字节就完成了一个字符。这个字符的大小为4
字节。
接下来的2
个字节将以0xDC
(二进制的110111
)开始。这使得4
个字节的值,保留12
位,20
位用于定义值。
有了这些保留值,字符就不能用0xD800
到0xDFFF
之间的值来表示,因为这样做会使其值与长度扩展相混淆。
UTF-32
二进制表示法¶
很明显,UTF-32
是如何工作的。它是直截了当的,没有任何需要提及的特殊情况。然而,重要的是要知道,在UTF-32
中的任何值都会将其第一个(最重要的)11
位作为0
。UTF
可能的值只包括21
位,而这11
位永远不会被使用。
值得注意的是,UTF-16
和UTF-32
并不向后兼容ASCII
,但UTF-8
却可以。这意味着用ASCII
编码保存的文件仍然可以用UTF-8
编码读取,但用其他两种编码则不能。
人工表示¶
字符串中的每个可表示的值都被命名为代码点或Unicode
标量。这些是同一事物的不同名称:一个特定字符的数字表示,如U+0061
。
每个数字都由不同的图画来表示,这被称为字符字形。UTF
,及其所有的变化,对每个Unicode标量到字形的映射都是一样的。这些标准只在机器如何表示该标量值方面有所不同。
例如,Unicode
标量U+0061
代表字母a
(拉丁文小写字母"a"
),U+00E9
代表é
(拉丁文小写字母"e"
带锐角)。
字体是一个具有不同画法的字形托盘。每个字形/字母都以不同的风格绘制,但最终,它们都有映射到一个二进制表示。字体只影响渲染;它不会改变存储的信息。
符号群¶
知道了UTF-8
和UTF-16
是如何表示可变大小的,你可以想象,知道一个字符串的长度并不像ASCII
和ANSI
表示法那样简单明了。对于后者,一个100
字节的数组只是100
个字符。对于UTF-8
和UTF-16
来说,这一点并不清楚,只有当你浏览所有的字节,找到其中有多少是扩展长度的表示时,你才会知道。对于UTF-32
来说,这不是一个问题。一个320
字节的字符串是一个10
个字符的字符串(包括结尾的nil
)。
说得复杂一点,假设你有4
个字节的UTF-16
字符串,而且没有扩展长度。你会认为,这意味着你有一个长度为2的字符串。答案是:不一定!
以字符U+00E9 é
(拉丁文小写字母"e"
带锐角)为例。它可以这样表示,也可以用两个Unicode
标量值表示,即标准字母e U+0065
(拉丁文小写字母"e"
)后跟U+0301
(结合锐角)。
打开一个新的playground
项目,并尝试如下:
import Foundation
let eAcute = "\u{E9}"
let combinedEAcute = "\u{65}\u{301}"
这就是两种表现形式,它们都代表着é
:
eAcute.count // 1
combinedEAcute.count // 1
在Swift
中,这两个字符串的长度都是1
,尽管它们的二进制大小不同。而且,这些字符串是相等的:
eAcute == combinedEAcute // true
当不同的Unicode
序列形成相同的结果时,这些结果被称为具有规范的等价性。Swift
平等性检查的是内容的规范等同性,而不是内容的绝对平等。
试着用Objective-C
类型来做同样的事情:
let eAcute_objC: NSString = "\u{E9}"
let combinedEAcute_objC: NSString = "\u{65}\u{301}"
eAcute_objC.length // 1
combinedEAcute_objC.length // 2
eAcute_objC == combinedEAcute_objC // false
Objective-C String
并没有读取任何东西。它只是比较了字节的内容。它没有检查它的内容,也没有弄清楚两者代表的是同一个东西。
Swift
中的一个字符并不像Objective-C
中那样代表一个字节。它代表的是一个字形群,可以是一个或多个标量值,组合起来代表一个字形。
如果你把两个通常会形成一个字形簇的字符分开,如果合并在一起,它们都会被当作普通字符。只有当你将它们合并时,它们才会成为一个不同的字符:
let acute = "\u{301}"
let smallE = "\u{65}"
acute.count // 1
smallE.count // 1
let combinedEAcute2 = smallE + acute
combinedEAcute2.count // 1
现在你明白了字符是如何用0
和1
表示的,让我们看看Swift
是如何处理它们的,以及所有这些"引擎盖下"的细节是如何影响你如何处理字符串的。
Swift
中的UTF
¶
在Swift 4.2
之前,Swift
使用UTF-16
作为首选编码。但由于UTF-16
与ASCII
不兼容,String
有两个存储编码:一个用于ASCII
,一个用于UTF-16
。Swift 5
及以后的版本只使用UTF-8
存储编码。
UTF-8
是最常见的服务器端编码。95%
以上的互联网都在使用它。你可能会想一想,互联网并不只有英语,UTF-16
是更合理的选择,因为会使用更少的扩展字节值。但是,大部分的网页都是HTML
,而HTML
可以完全用ASCII
表示。这使得互联网内容使用UTF-8
在尺寸和传输速度方面成为更好的选择。也就是说,改用UTF-8
存储编码后,Swift
和服务器之间的任何通信都变得简单明了,因为它们使用相同的编码,因此不需要转换。
收集协议的一致性¶
String
符合两个集合协议。BidirectionalCollection
和RangeReplaceableCollection
:
var sampleString = "Lo͞r̉em̗ ȉp͇sum̗ do͞l͙o͞r̉ sȉt̕ a͌m̗et̕"
sampleString.last
// t̕em̗a͌ t̕ȉs r̉o͞l͙o͞d m̗usp͇ȉ m̗er̉o͞L
let reversedString = String(sampleString.reversed())
if let rangeToReplace = sampleString.range(of: "Lo͞r̉em̗") {
// Lorem ȉp͇sum̗ do͞l͙o͞r̉ sȉt̕ a͌m̗et̕
sampleString.replaceSubrange(rangeToReplace,
with: "Lorem")
}
你可以在任何方向上遍历一个Swift String
,你也可以替换一个范围的值。但是它不符合RandomAccessCollection
。
你可以用subscript(_:)
来扩展String
,这样你就可以很容易地通过其索引来访问字符:
extension String {
subscript(position: Int) -> Self.Element {
get {
let characters = Array(self)
return characters[position]
}
set(newValue) {
let startIndex = self.index(self.startIndex,
offsetBy: position)
let endIndex = self.index(self.startIndex,
offsetBy: position + 1)
let range = startIndex..<endIndex
replaceSubrange(range, with: [newValue])
}
}
}
那么下面的代码将起作用:
sampleString[2] // r
sampleString[2] = "R"
sampleString // LoRem ȉp͇sum̗ do͞l͙o͞r̉ sȉt̕ a͌m̗et̕
在上面的代码中,似乎没有什么问题。试试下面的代码:
for i in 0..<sampleString.count {
sampleString[i].uppercased()
}
简单看一下,你会认为这段代码的复杂度是O(n)
,但这是不正确的。在subscript(_:)
的实现中,你将字符串转换为数组来获得你想要的索引。这本身就是一个O(n)
操作,所以你添加的循环的复杂度为O(n^2)
。
如果不先经过n-1
个字符,你就无法直接到达n
个字符。一个字符--又称字素群--可以是一长串Unicode
标量,使得到达第n
个字符的操作是O(n)
,而不是O(1)
,因此不符合`随机访问集合'的要求。
虽然你创建的扩展简化和缩短了你的代码,但也影响了性能:
for element in sampleString {
element.uppercased()
}
这段代码是一样的。它没有使用下标的方法,而是遍历了一次集合。使用下标的方法往往会显得很有吸引力,但这种方法导致你做的操作比你想象的多得多。因此,了解String
类是如何工作的,以及什么是Character
和Swift
如何对待它,可以使你在处理挑战和实施解决方案时有很大的不同。
字符串排序¶
你已经很熟悉字符串的比较了。字符串中的默认排序忽略了本地化的偏好。
字符串比较始终是一致的,这是它应该做的。然而,对于不同的本地化,它应该是不同的。
例如,在德语和瑞典语之间,Ö
的排序与Z
是不同的:
let OwithDiaersis = "Ö"
let zee = "Z"
OwithDiaersis > zee // true
// German 🇩🇪
OwithDiaersis.compare(
zee,
locale: Locale(identifier: "DE")) == .orderedAscending // true
// Sweden 🇸🇪
OwithDiaersis.compare(
zee,
locale: Locale(identifier: "SE")) == .orderedAscending // false
当你为系统内部使用而订购文本时,地域性不能影响它。但如果你是为了向用户展示它而排序,你就必须意识到这些差异。
另外,当字符串中有数字时,有一个臭名昭著的问题。一个数值为"11"
的字符串应该比一个数值为"2 "
的字符串高。但事实并非如此,除非是考虑到地域性的比较。
"11".localizedCompare("2") == .orderedAscending // true
"11".localizedStandardCompare("2") == .orderedAscending // false
字符串折叠¶
你越是与不同的语言打交道,你在字符串搜索方面就会面临更多的挑战。你现在知道字母é
(拉丁文小写字母"e"
带锐角)的不同表示方法。但是"Café"
这个词与"Cafe"
不匹配:
"Café" == "Cafe" // false
而检查它是否包含字母e
(拉丁文小写字母"e"
)将返回错误:
"Café".contains("e") // false
在一个字符上使用变音符,可以把它变成一个不同的字符。虽然它的来源是一样的,但将它与原始的比较会失败--几乎是不同情况下的相同想法:
"Café" == "café" // false
"Café".contains("c") // false
当你想对字符串进行比较,并忽略大小写时,你将原始字符串和关键词转换为相同的大小写,大写或小写。这被称为字符串折叠,你去除字符串上的区别,使其适合于比较。
在变音符的情况下,你要删除所有的标记,并将所有的字符返回到它们的原始字母,以简化比较。继续我们的例子,这将使Café
,或其任何其他变音,返回为Cafe
。
请看下面的例子:
let originalString = "H̾e͜l͘l͘ò W͛òr̠l͘d͐!"
originalString.contains("Hello") // false
originalString
在字符串Hello World!
中的每个字母都包含一个组合字符。这使得它很难搜索到任何单词。幸运的是,String
提供了一个折叠机制,所以你可以指定你想删除哪些区别。大小写、变音符,或者两者都有:
let foldedString = originalString.folding(
options: [.caseInsensitive, .diacriticInsensitive],
locale: .current)
foldedString.contains("hello") // true
folding(options:locale:)
中的选项参数给了你这种控制。在这个例子中,它同时删除了大小写和变音。得到的字符串是hello world!
。
另一个更短的方法是使用localizedStandardContains(_:)
来实现:
originalString.localizedStandardContains("hello") // true
这个方法也是如此。它执行了一个对大小写不敏感的、有本地意识的比较。如果不对字符串进行折叠以去除变音符,你将很难搜索到文本,或者你会给用户带来非常不愉快的体验。
字符串和子串在内存中¶
另一个与String
的性能有关的棘手点是Substring
。就像String
如何符合StringProtocol
一样,Substring
也是如此。
从它的名字可以看出,子串是一个字符串的一部分。当你要分解一个大字符串时,它是一个非常快速和优化的数据类型。然而,有一个关键点是你应该注意的,特别是在处理大字符串的时候:
func doSomething() -> Substring {
let largeString = "Lorem ipsum dolor sit amet"
let index = largeString.firstIndex(of: " ") ?? largeString.endIndex
return largeString[..<index]
}
上面的代码返回一个大字符串的第一个字。
你通过快速查看所期望的是,你处理了大字符串,完成了对它的使用,并且只返回了你需要的字符串的一小部分:
let subString = doSomething() // Lorem
subString.base // "Lorem ipsum dolor sit amet"
你仍然有大的字符串加载在内存中。Substring
与原始字符串共享内存。如果你正在处理一个大的字符串,并且需要从它那里得到很多小的字符串,同时仍然使用这个大的字符串,就不会有额外的内存成本。但是如果你想直接打破它,从内存中删除大字符串,那么你需要马上从你的子串中创建一个新的字符串对象:
let newString = String(subString)
如果你不这样做,原来的字符串会在你不知道的情况下在记忆中停留更长时间。
这是关于String
的很多信息。下一部分将介绍你经常使用的Swift的一个非常有趣的福利。你会知道它是如何在引擎盖下工作的,并在其基础上进行构建。
自定义字符串插值¶
字符串插值是一个创建字符串的强大工具。但它并不局限于创建字符串。是的,当然,它包括字符串,但你可以用它来通过字符串构造一个对象。是的,我知道这很让人困惑。
考虑一下下面这个类型:
struct Book {
var name: String
var authors: [String]
var fpe: String
}
如果你能从Book
中定义一个新的实例,并写上"Expert Swift by: Ehab Amer,Marin Bencevic,Ray Fix,Shai Mishali"
?
Swift
允许你通过符合协议ExpressibleByStringLiteral
和实现init(stringLiteral value: String)
来用字符串字面定义任何类型。
添加这个扩展:
extension Book: ExpressibleByStringLiteral {
public init(stringLiteral value: String) {
let parts = value.components(separatedBy: " by: ")
let bookName = parts.first ?? ""
let authorNames = parts.last?.components(separatedBy: ",") ?? []
self.name = bookName
self.authors = authorNames
self.fpe = ""
}
}
You break down the string into two parts with the " by: "
separator: The first part is the book name, and the second part is the author names, comma-separated. Ignore the “fpe” (final pass editor) for now, but you’ll use this property later.
The string defining the book should be [Book name] + by: + Author1,Author2,Author3,….:
你用"by:"
将字符串分解成两部分。分隔符。第一部分是书名,第二部分是作者姓名,用逗号分隔。暂时忽略"fpe"(final pass editor)
,但你以后会用到这个属性。
定义书的字符串应该是[Book name] + by: + Author1,Author2,Author3,…
:
var book: Book = """
Expert Swift by: Ehab Amer,Marin Bencevic,\
Ray Fix,Shai Mishali
"""
book.name // Expert Swift
book.authors.first // Ehab Amer
这是一种非常人性化的方式来构建你的对象,但如果在这种格式下有任何改变,意外的数据将被保存在对象中!这就是为什么我们要把这些数据保存在对象中!
var invalidBook: Book = """
Book name is `Expert Swift`. \
Written by: Ehab Amer, Marin Bencevic, \
Ray Fix & Shai Mishali
"""
invalidBook.name // Book name is `Expert Swift`. Written
invalidBook.authors.last // Ray Fix & Shai Mishali
现在,这个名字包含了无效的信息,最后一个作者实际上是两个人在一起。你可以通过改进init(stringLiteral value: String)
的实现来解决这个问题,但是你是否能够期待所有可能的输入,以确保字符串能够被正确解析?
还有一种方法可以构造Book
:使用字符串插值。要做到这一点,你要定义一个字符串,其中清楚、明确地提到书名和作者数组:
extension Book: ExpressibleByStringInterpolation { // 1
struct StringInterpolation: StringInterpolationProtocol { // 2
var name: String // 3
var authors: [String]
var fpe: String
init(literalCapacity: Int, interpolationCount: Int) { // 4
name = ""
authors = []
fpe = ""
}
mutating func appendLiteral(_ literal: String) { // 5
// Do something with the literals?
}
mutating func appendInterpolation(_ name: String) { // 6
self.name = name
}
mutating func appendInterpolation(
authors list: [String]) { // 7
authors = list
}
}
init(stringInterpolation: StringInterpolation) { // 8
self.authors = stringInterpolation.authors
self.name = stringInterpolation.name
self.fpe = stringInterpolation.fpe
}
}
- 要使用自定义插值来定义一个
Book
,你需要它符合ExpressibleByStringInterpolation
。 - 这需要定义一个名称为
StringInterpolation
的结构,符合StringInterpolationProtocol
。这个结构的可见性仅来自于Book
类型中。 - 新结构必须携带属性以存储将在字符串中提供的值。在这个例子中,
name
和authors
就可以了。你也可以拥有你可能需要的任何属性。暂时忽略fpe
。 - 该字符串将包含字面字符串和插值。这个初始化器是第一个被调用的。它提供了字面意义上的每个字符的计数和插值的数量。
- 对于字符串中的字面符号,这个被调用。在这个例子中,对它们不做任何处理。这个方法声明确定了字面的通用类型为
String
。 - 这个方法添加了一个定义书名的字符串的插值。插值应该看起来像
"(String)"
。 - 这增加了一个插值签名,看起来像
"\(authors: [String])"
。这是对作者列表的标签化插值。 - 你定义了一个新的初始化器,其参数类型为
StringInterpolation
,这与你定义的结构相同。
现在你可以像这样创建一个Book
的实例:
var interpolatedBook: Book = """
The awesome team of authors \(authors:
["Ehab Amer", "Marin Bencevic", "Ray Fix", "Shai Mishali"]) \
wrote this great book. Titled \("Expert Swift")
"""
这本书的定义有了更多的描述。甚至作者名单也在书名之前。但是因为每个插页都有它的形式,或者通过标签和/或数据类型,所以没有发生混淆。
幕后实际发生的情况如下:
let stringInterpolation = StringInterpolation(
literalCapacity: 59,
interpolationCount: 2)
stringInterpolation.appendLiteral("he awesome team of authors ")
stringInterpolation.appendInterpolation(
authors: ["Ehab Amer",
"Marin Bencevic",
"Ray Fix",
"Shai Mishali"])
stringInterpolation
.appendLiteral(" wrote this great book. Titled ")
stringInterpolation
.appendInterpolation("Expert Swift")
Book(stringInterpolation: stringInterpolation)
init(literalCapacity: Int, interpolationCount: Int)
被调用,输入总字数和插值的数量。
然后,对于每个字面序列,appendLiteral(_:)
被调用。之后,对于每个插值,都会调用其相应的方法。最后,用插值对象调用初始化器。
请注意,每个插值都被翻译成一个方法。(_:)
被翻译成appendLiteral(_:)
,而(authorities:)
被翻译成appendLiteral(authorities:)
。
还记得你没有使用的fpe
吗?到目前为止,你只关注书的标题和作者。但是在创建插值对象的时候,你对这个属性没有用处,所以把它空着。
给StringInterpolation
添加一个扩展,定义在Book
里面:
extension Book.StringInterpolation {
mutating func appendInterpolation(fpe name: String) {
fpe = name
}
}
然后,用这个内插法定义一个新的书:
var interpolatedBookWithFPE: Book = """
\("Expert Swift") had an amazing \
final pass editor \(fpe: "Eli Ganim")
"""
这创建了一个新的书的实例,并使用你在扩展中确定的插值来设置fpe
。你可以根据自己的意愿定义更多的插值格式:
extension Book.StringInterpolation {
mutating func appendInterpolation(bookName name: String) {
self.name = name
}
mutating func appendInterpolation(anAuthor name: String) {
self.authors.append(name)
}
}
这增加了另一种定义书名的方法和逐一添加作者的方法:
var interpolatedBook2: Book = """
\(anAuthor: "Ray Fix") & \(anAuthor: "Shai Mishali") \
were authors in \(bookName: "Expert Swift")
"""
String
类型与Book
没有区别。你已经用它的StringInterpolation
子类型做了一段时间的标准插值,比如在一个字符串中包含一个数字:
var num = 1234
var string = "The number is: \(num)"
正如你在Book
上为fpe添加
了一个新的插值,你也可以在String
上做同样的事情来插值Book
。
试着将第一个书的实例纳入一个字符串中:
var string = "\(book)"
// Book(name: "Expert Swift", authors: ["Ehab Amer", "Marin Bencevic", "Ray Fix", "Shai Mishali"], fpe: "")
字符串没有对书的友好表示。但你可以控制这一点。在String
里面添加一个StringInterpolation
的扩展:
extension String.StringInterpolation {
mutating func appendInterpolation(_ book: Book) {
appendLiteral("The Book \"")
appendLiteral(book.name)
appendLiteral("\"")
if !book.authors.isEmpty {
appendLiteral(" Authored by: ")
for author in book.authors {
if author == book.authors.first {
appendLiteral(author)
} else {
if author == book.authors.last {
appendLiteral(", & ")
appendLiteral(author)
appendLiteral(".")
} else {
appendLiteral(", ")
appendLiteral(author)
}
}
}
}
if !book.fpe.isEmpty {
appendLiteral(" Final Pass Edited by: ")
appendLiteral(book.fpe)
}
}
}
将fpe添加到你之前定义的interpolatedBook
对象中,并将其转换为一个字符串:
interpolatedBook.fpe = "Eli Ganim"
var string2 = "\(interpolatedBook)"
// The Book "Expert Swift" Authored by: Ehab Amer, Marin Bencevic, Ray Fix, & Shai Mishali. Final Pass Edited by: Eli Ganim
现在,这是一种更友好的描述一本书的方式。
在扩展中,你可以完全控制字段的打印方式、顺序以及每个属性前面和/或后面的用户友好文本。
这里大量使用appendLiteral(_:)
的原因是,你不知道String.StringInterpolation
的内部实现,你也不知道它有哪些临时字段来存储信息。但是它不像Book.StringInterpolation
。字面信息就像插值一样被存储,而且是按顺序存储的,所以你可以安全地将插值转换为一系列的字面信息。最后,它只是一个字符串。而不是像Book
中那样有多个字段。
关键点¶
ASCII
是存储字符的第一个标准,它发展到UTF
,在一个单一的标准中表示所有可能的字符。UTF-8
和UTF-16
都可以通过可变大小的表示法来表示21
比特的不同数值。一个UTF-8
字符最多可以占用4
个字节。UTF-16
和UTF-32
并不向后兼容ASCII
。UTF-8
是互联网上最受欢迎的编码,因为它代表一个网页的大小较小。- 字素簇可以是一个或多个不同的
Unicode
值合并在一起,形成一个字形。 Swift
中的一个字符是一个字母簇,而不是一个Unicode
值。而同一个集群可以用不同的方式表示。这被称为典型的等价。- 要达到一个字符串中的第
n
个字符,你需要通过它之前的n-1
个字符。这不是的O(1)
操作。 - 字符串的顺序可以根据地区性的不同而不同。
- 字符串折叠是去除任何字符的区别以方便比较。
Substring
的性能很高,因为它不分配新的内存来引用找到的那部分字符串。然而,这意味着原始字符串仍然存在于内存中。- 你可以直接从一个字符串实例化一个对象,可以是字面意思,也可以是插值。
- 你也可以为
String
提供你的自定义类型的新插值,以便对其字符串表示有更多的控制。