Swift4.2 字符串和字符

Swift中的字符串都是String类型的,字符是Character类型,看似简单,其中的细节可不少(•́ω•̀ ٥)

Swift 字符串是由 String 类型来表示。 String 的内容可以用多种方式读取,包括作为一个 Character 值的集合。

注意
Swift 的字符串类型与 Foundation 的 NSString 类型进行了无缝桥接。 Foundation 也可以对 String 进行扩展,暴露在 NSString 中定义的方法。 这就意味着,你可以不用进行类型转换,就能在 String 中调用 NSString 的这些方法。
更多关于在 Foundation 和 Cocoa 中使用 String 的信息,查看 Bridging Between String and NSString

初始化一个空字符串

创建一个空 String 有两种方式,给一个变量赋值一个空字符串或者使用下面的语法初始化一个 String 实例对象:

1
2
3
var emptyString = ""               // 空字符串
var anotherEmptyString = String() // 初始化语法
//这是两个空字符串,他们等价

可以通过检查 String 的布尔类型的属性 isEmpty 来判断该字符串的值是否为空:

1
2
3
4
if emptyString.isEmpty {
print("Nothing to see here")
}
// 打印 "Nothing to see here"

字符串是值类型

Swift 中的 String 类型是一种 值类型 。如果你创建了一个新的 String 值, String 值在传递给方法或者函数时会被 拷贝,在给常量或者变量赋值的时候也是一样。在任何情况下,都会对现存的 String 值创建新拷贝,并对新拷贝进行传递或赋值操作。值类型在 结构体和枚举是值类型 中有详细描述。

Swift 默认 String 拷贝的行为是为了保证在函数或方法中传递的是 String 值,不管该值是从哪里来,你都绝对拥有这个 String 值。你可以确定你传递的这个字符串不会被修改,除非你自己去修改它。

另一方面,Swift 编译器优化了字符串的使用,实际拷贝只会在需要的时候才进行。这意味着你把字符串当做值类型的同时也能够得到很棒的性能。

使用字符

你可以使用 for-in 循环来遍历 String 中每个的 Character 的值:

1
2
3
for character in "Dog!🐶" {
print(character)
}

你可以使用 Character 类型声明,并赋值一个单字符值创建一个独立的字符常量或变量:

1
let exclamationMark: Character = "!"

String 的值可以使用一个 Character 值类型的数组作为变量来进行初始化:

1
2
3
4
let catCharacters: [Character] = ["C", "a", "t", "!", "🐱"]
let catString = String(catCharacters)
print(catString)
// 输出 "Cat!🐱"

字符串和字符的拼接

可以使用加号( + )将 String 的值加(或 拼接 )在一起创造出一个新的值

你可以使用加等于赋值符号( += )将一个 String 的值追加到一个已经存在的 String 变量中

你可以使用 Stringappend() 方法将一个 Character 的值追加到一个 String 变量中

注意
你不能将字符串 String 或字符 Character 拼接到 Character 变量中,因为 Character 的值只能包含单个字符。

字符计数

在一个字符串中使用 count 属性去计算 Character 类型值个数

注意,Swift 对 Character 类型值使用了拓展字母集,意味着字符串的拼接和修改不一定会持续影响字符串字符个数。

例如,你初始化一个拥有四个字符的字符串 cafe,然后再追加一个 COMBINING ACUTE ACCENT (U+0301) 字符在末尾 ,最终形成的字符串还是拥有四个字符,并且最后一个字符是 ,而不是 e

1
2
3
4
5
6
7
8
var word = "cafe"
print("the number of characters in \(word) is \(word.count)")
// 打印 "the number of characters in cafe is 4"

word += "\u{301}" // 拼接重音符,U+0301

print("the number of characters in \(word) is \(word.count)")
// 打印 "the number of characters in café is 4"

注意
拓展字母集可以由多个不同的 Unicode 标量组成,这就意味着相同字符和相同字符的不同表示需要占据不同的内存空间去存储,因此,在字符串的各种表示中 Swift 字符占据的内存并不一样。造成的结果就是,字符串的字符数量并不能通过遍历该字符串去计算,并用于确定该字符串的拓展字符集边界。如果你正在处理特别长的字符串,要意识到为了确定该字符串的字符个数, count 属性必须要遍历完整个字符串中的全部 Unicode 标量。

count 属性返回的字符个数不会一直都与包含相同字符的 NSStringlength 属性返回的字符个数相同。 NSString 的长度是基于 UTF-16 表示的字符串所占据的 16 位代码单元的个数决定,而不是字符串中的拓展字母集个数决定。

访问和修改字符串

你可以通过字符串的方法和属性来访问和修改它,或者通过下标语法。

字符串索引

每个 String 值都有一个关联的 索引类型, String.Index,对应着字符串中每个 Character 的位置。

正如上面提到的,不同的字符可能需要不同大小的内存存储,所以为了确定每个 Character 的具体位置,你必须从 String 的开头遍历每个 Unicode 标量到结束。因此,Swift 字符串不能使用整型值索引。

使用 startIndex 属性可以访问 String 的第一个 Character 的位置。使用 endIndex 属性可以访问 String 的最后一个 Character 的位置。因此, endIndex 属性并不是字符串下标的有效参数。如果 String 是空串, startIndexendIndex 就是相等的。

你可以通过使用 Stringindex(before:)index(after:) 方法,访问给定索引的前一个和后一个索引。要访问离给定索引偏移较多的索引,你可以使用 index(_:offsetBy:) 方法,避免多次调用 index(before:)index(after:) 方法。

使用 indices 属性会创建一个包含全部索引的范围,用来在一个字符串中访问单个字符。

1
2
3
4
for index in greeting.indices {
print("\(greeting[index]) ", terminator: "")
}
// 打印 "G u t e n T a g ! "

注意
你可以在任意一个遵循 Collection 协议的类型里面,使用 startIndexendIndex 属性或者 index(before:)index(after:)index(_:offsetBy:) 方法。如上文所示是使用在 String 中,你也可以使用在 ArrayDictionarySet 中。

插入和删除

在一个字符串指定位置插入单个字符,使用 insert(:at:) 方法,而要插入另一个字符串的内容时,使用 insert(contentsOf:at:) 方法。

删除一个字符串指定位置的单个字符,用 remove(at:) 方法,而要删除指定范围的子字符串时,用 removeSubrange(_:)

注意
你可以在任何遵循 RangeReplaceableCollection 协议的类型上使用 insert(_:at:)insert(contentsOf:at:)remove(at:),和 removeSubrange(_:) 方法。除了这里说到的 String,还包括 ArrayDictionary,和 Set 等集合类型。

子字符串

当你从字符串中获取一个子字符串 —— 例如使用下标或者 prefix(_:) 之类的方法 —— 就可以得到一个 Substring 的 实例 ,而非另外一个 String 。Swift 里的 Substring 的绝大部分函数都跟 String 一样,意味着你可以使用同样的方式去操作 SubstringString 。然而,跟 String 不同的是,你只有在短时间内需要操作字符串时,才会使用 Substring 。当你需要长时间保存结果时,就把 Substring 转化为 String 的实例:

1
2
3
4
5
6
7
let greeting = "Hello, world!"
let index = greeting.firstIndex(of: ",") ?? greeting.endIndex
let beginning = greeting[..<index]
// beginning 的值是 "Hello"

// 把结果转化为 String 以便长期存储。
let newString = String(beginning)

就像 String ,每一个 Substring 都会在内存里保存字符集。而 StringSubString 的区别在于性能优化上,Substring 可以重用原 String 的内存空间,或者另一个 Substring 的内存空间(String 也有同样的优化,但如果两个 String 共享内存的话,它们就会相等)。这一优化意味着你在修改 StringSubstring 之前都不需要消耗性能在内存复制。就像前面说的那样,Substring 不适合长期存储 —— 因为它重用了原 String 的内存空间,原 String 的内存空间必须保留直到它的 Substring 不再被使用为止。

上面的例子, greeting 是一个 String,意味着它在内存里有一片空间保存字符集。而由于 beginninggreetingSubstring,它重用了 greeting 的内存空间。相反,newString 是一个 String —— 它是使用 Substring 创建的,拥有一片自己的内存空间。下面的图展示了他们之间的关系:

注意
StringSubstring 都遵循 StringProtocol协议, 这意味着操作字符串的函数使用 StringProtocol 会更加方便。你可以传入 StringSubstring 去调用函数。

比较字符串

Swift 提供了三种方式来比较文本值: 字符串和字符相等、前缀相等、后缀相等。

字符串和字符相等

如果他们的扩展字形集是 统一码等价,则这两个 String 值 (或者两个 Character 值) 被认为是等同的。如果它们具有相同的语言含义和外观,即使它们是由不同语义的 Unicode 标量组成,扩展字形集也是等同的。

例如,LATIN SMALL LETTER E WITH ACUTE (U+00E9) 在规范上等同于 LATIN SMALL LETTER E (U+0065) 加上 COMBINING ACUTE ACCENT (U+0301)。这两个扩展字形簇都是表示字符 é 的有效方法,因此它们被认为是规范等价的

相反,英文中的 LATIN CAPITAL LETTER A (U+0041,或 「A」),和俄文中的 CYRILLIC CAPITAL LETTER A (U+0410, 或 「А」) 不相等。这两个字符在视觉上相似,但具有不同的语言含义

注意
Swift 中的字符串和字符比较不是区域敏感的。

前缀和后缀比较

可以使用字符串的 hasPrefix(_:)hasSuffix(_:) 方法来检查一个字符串是否有特定的前缀、后缀。这两个方法接收一个 String 类型的参数返回一个布尔值。

注意
hasPrefix(_:)hasSuffix(_:) 方法都是在每个字符串的扩展字符集中逐个字符进行比较, 如本文所述 字符串和字符的比较。

字符串的 Unicode 表示形式

当一个 Unicode 字符串被写入文本文件或者一些其他存储时,字符串中的 Unicode 标量会用 Unicode 定义的几种 编码格式 编码。每一个字符串中的小块编码都叫做 代码单元。这些包括 UTF-8 编码格式 (编码字符串为 8 位的代码单元),UTF-16 编码格式 (编码字符串为16位的代码单元) , 以及 UTF-32 编码格式 (编码字符串32位的代码单元) 。

Swift 提供几种不同的方式来访问字符串的 Unicode 表现形式。 你可以使用 for - in 对字符串进行便利, 进而访问其中单个 Character 字符值作为 Unicode 扩展的字符群集。 这个过程描述在 使用字符。

另外,也可以通过其他三种 Unicode 兼容的方式访问字符串的值:

  • UTF-8 代码单元集合(利用字符串的 utf8 属性进行访问)
  • UTF-16 代码单元集合 (利用字符串的 utf16 属性进行访问)
  • 21 位的 Unicode 标量值集合,也就是字符串的 UTF-32 编码格式(利用字符串的 unicodeScalars 属性进行访问)
    下面有 Dog , !!DOUBLE EXCLAMATION MARK ,或Unicode 标量 U+203C )和 🐶DOG FACE,Unicode 标量为 U+1F436)组成的字符串中的每一个字符代表着一种不同的表示:
    1
    let dogString = "Dog‼🐶"

UTF-8 表示形式

你可以通过遍历 Stringutf8 属性来访问他的 UTF-8 表示。这个属性是 string.UTF8View 类型的,UTF8View 是无符号 8 位( UInt8 )值得集合,每一个字节都对应一个字符串的 UTF-8 的表现形式:

1
2
3
4
5
for codeUnit in dogString.utf8 {
print("\(codeUnit) ", terminator: "")
}
print("")
// 打印 "68 111 103 226 128 188 240 159 144 182 "

上面的例子中,前三个 10 进制 codeUnit 值(68111103)代表了字符 D og ,他们的 UTF-8 表示和 ASCII 表示相同。接下来的三个 10 进制 codeUnit 值(226128, 188)是 DOUBLE EXCLAMATION MARK 的 3 字节 UTF-8 表示形式。 最后四个 codeUnit 值 (240, 159, 144, 182) 是 DOG FACE 的 4 字节 UTF-8 表示形式。

UTF-16 表示形式

你可以通过遍历 Stringutf16 属性来访问它的 UTF-16 表示形式。它是 String.UTF16View 类型的属性, 它是一个无符号 16 位 (UInt16) 值的集合,每一个 UInt16 都是一个字符的 UTF-16 表示形式:

1
2
3
4
5
for codeUnit in dogString.utf16 {
print("\(codeUnit) ", terminator: "")
}
print("")
// 打印 "68 111 103 8252 55357 56374 "

同样,前三个 codeUnit 值 (68, 111, 103) 代表了字符 D, o, 和 g, 他们的 UTF-16 代码单元和 UTF-8 完全相同 (因为这些 Unicode 标量表示 ASCII 字符)。

第四个 codeUnit 值 (8252) 是一个等于十六进制 203C 的十进制值,
这代表了 DOUBLE EXCLAMATION MARK 字符的 Unicode 标量值 U+203C 。这个字符在 UTF-16 中可以用一个代码单元表示。

第五个和第六个 codeUnit 值 (5535756374) 是 DOG FACE 字符的 UTF-16 表示形式。 第一个值为 U+D83D (十进制值为 55357 ) 第二个值为 U+DC36 (十进制值为 56374 )。

Unicode 标量表示形式

你可以通过遍历 String 值的 unicodeScalars 属性来访问它的 Unicode 标量表示。 它是一个 UnicodeScalarView 类型的属性, UnicodeScalarViewUnicodeScalar 类型的值得集合。

每一个 UnicodeScalar都有一个 value属性,可以返回对应的 21 位数值,用 UInt32 值来表示:

1
2
3
4
5
for scalar in dogString.unicodeScalars {
print("\(scalar.value) ", terminator: "")
}
print("")
// 打印 "68 111 103 8252 128054 "

前三个 UnicodeScalar 值 (68, 111, 103) 的 Value 属性依旧代表着字符 D, o, and g

第四个 codeUnit 值 (8252) 依旧是一个等于十六进制 203C 的十进制值, 这代表了 DOUBLE EXCLAMATION MARK字符的 Unicode 标量 U+203C

第五个 UnicodeScalar 值的 Value 属性, 128054, 是一个十六进制 1F436 的十进制表现, 它代表 DOG FACE 字符的 Unicode 标量 U+1F436

作为查询他们的 value属性的一种替代方法, 每一个 UnicodeScalar 值也可以用来构建一个新的 String 值, 比如在字符串插值中使用:

1
2
3
4
5
6
7
8
for scalar in dogString.unicodeScalars {
print("\(scalar) ")
}
// D
// o
// g
// ‼
// 🐶