字符串
- Swift 字符串不支持随机访问
Unicode,而非固定宽度
今天的 Unicode 是一个可变长格式。它的可变长特性有两种不同的意义:
- 一个 Unicode 字符,也叫做扩展字位簇 (extended grapheme cluster),由一个或多个 Unicode 标量 (Unicode scalar) 组成。
- 一个 Unicode 标量可以被编码成一个或多个编码单元 (code units)。
Unicode 中组基础的原件叫做编码点 (code point):它是一个位于 Unicode 编码空间 (从 0 到 0x10FFFF,也就是十进制的 1,114,111) 中的整数。
Unicode 标量 和当才提到的编码点,在绝大多数情况下,是同一个东西。或者说,除了编码点中 0xB800 - 0xDFFF 之外的值,都可以叫做 Unicode 标量。而 0xD800 - 0xDFFF 这2048个值则叫做 代理编码点(surrogate code points),它们在 UTF-16 编码中用于表示那些值大于 65525 的字符。
同样的 Unicode 数据可以用多种不同的编码方式进行编码,其中最普遍使用的是 8 比特 (UTF-8) 和 16 比特 (UTF-16)。编码方式中使用的最小实体叫做编码单元,也就是说UTF-8编码单元的宽度是8比特,而UTF-16编码单元的宽度是16比特。
用户在屏幕上看到的“单个字符”,可能是由多个Unicode标量组合起来的。对于这种用户感知到的“单个字符”,Unicode 中有一个术语,叫做 (扩展)字位簇,对应的英文叫做 (extended) grapheme cluster。
字位簇和标准等价
合并标记
let single = "Pok\u{00E9}mon" // Pokémon
let double = "Poke\u{0301}mon" // Pokémon
(single, double) // ("Pokémon", "Pokémon")
single.count // 7
double.count // 7
single == double // true
single.unicodeScalars.count // 7
double.unicodeScalars.count // 8
颜文字
- Unicode 把复杂的颜文字表示成一个简单的颜文字的序列,序列中的颜文字则通过一个标量值为 U+200D 的不可见
零宽连接字符 (zero-width joiner, ZWJ)连接。
字符串和集合
双向索引,而非随机访问
- String 不是一个可以随机访问的集合。就算知道给定字符串中第 n 个字符的位置,也并不会对计算这个字符之前有多少个 Unicode 标量有任何帮助。所以,String 只实现了 BidirectionalCollection。你可以从字符串的头或者尾开始,向后或者向前移动,代码会察看毗邻字符的组合,跳过正确的字节数。不管怎样,你每次只能迭代一个字符。
范围可替换,而非可变
-
String 还满足 RangeReplaceableCollection 协议。
var greeting = "Hello, world!" if let comma = greeting.index(of: ",") { greeting[..<comma] // Hello greeting.replaceSubrange(comma...,with: " again.") } greeting // Hello again.
字符串索引
-
Swift 不允许使用整数值对字符串进行下标操作。因为整数的下标访问无法在常数时间内完成 (对于 Collection 协议来说这也是个直观要求),而且查找第 n 个 Character 的操作也必须要对她之前的所有字节进行检查。
-
操作字符串索引的 API 与你在遇到其他任何集合时使用的索引操作是一样的,它们都基于 Collection 协议。
let s = "abcdef" let second = s.index(after: s.startIndex) s[second] // b -
扩展分隔符语法(extended delimiters syntax),就是把字符串用 # 包围起来。这样就可以在字符串直接使用引用而不用转义了。let scv = #""" "Value in quotes","can contain, characters" "Values without quotes work as well:", 42 """#
子字符串
- 和所有集合类型一样,String 有一个特定的 SubSequence 类型,叫做 Substring。Substring 和 ArraySlice 很相似:它是一个以原始字符串内容为基础,用不同起始和结束位置标记的视图。
StringProtocol
-
Substring 和 String 的接口几乎完全一样。这是通过一个叫做 StringProtocol 的通用协议来达到的,String 和 Substring 都遵守这个协议。
func lastWord(in input: String) -> String? { // 处理输入,操作子字符串 let words = input.split(separators: [","," "]) guard let lastWord = words.last else { return nil } // 转换为字符串并返回 return String(lastword) } lastWord(in: "one, two, three, four, five") // Optional("five") -
不鼓励长期存储字符串的根本原因在于,子字符串会一直持有整个原始字符串。
-
通过在一个操作内部使用子字符串,而只在结束时创建新字符串,我们将复制操作推迟到最后一刻,这可以保证由这些复制操作所带来的的开销是实际需要的。
-
在大部分 API 中只使用 String, 而不是将它换为泛型 (其实泛型本身也会带来开销),会更加简单和清晰。但那些极有可能处理子字符串,同时又无法进一步泛型化成 Sequence 或 Collection 一般操作的API,则不适用上述规则。
extension Sequence where Element: StringProtocol { /// 将一个序列中的元素使用给定的分隔符拼接起来为新的字符串,并返回 public func joined(separator: String = "") -> String } -
如果你想要扩展 String 为其添加新的功能,将这个扩展放在 StringProtocol 会是一个好主意,这可以保持 String 和 Substring API 的统一性。StringProtocol 设计之初就是为了在你想要对 String 扩展时来使用的。
-
StringProtocol 并不是一个你想要构建自己的字符串类型时所应该实现的目标协议。
不要声明任何新的遵守 StringProtocol 协议的类型。只有标准库中的 String 和 Substring 是有效的适配类型。
编码单元视图
- 有时候字位簇无法满足需要时,我们还可以向下到比如 Unicode 标量或者编码单元这样更低的层次中进行查看和操作。String 为此提供了三种视图: unicodeScalars, utf16 和 utf8。
- UTF-8 是用来存储或者在网络上发送文本的事实标准。UTF-8 视图是 String 所有编码单元视图中,系统开销最低的。因为它是 Swift 字符串在内存中的原生存储格式。
- 要注意的是,utf8 集合不包含字符串尾部的 null 字节。如果你需要用 null 表示结尾的话,可以使用 String 的 withCString 方法或者 utf8CString 属性。
共享索引
- 字符串和它们的视图共享同样的索引类型,String.Index。
字符串和 Foundation
- Swift 的 String 类型和 Foundation 的 NSString 有着非常密切的关系。任意的 String 实例都可以通过 as 操作桥接转换为 NSString,而且那些接受或者返回 NSString 的 Objective-C API 也会把类型自动转换为 String。
- Swift 字符串在内存中的原生编码是 UTF-8,而 NSString 是UTF-16,这种差异会导致 Swift 字符串桥接到 NSString 时会有一些额外的性能开销。对于 NSString,在以 UTF-16 计算的偏移上移动位置消耗的是常量时间,而这对于 Swift 字符串来说,是一个花费线性时间的操作。为了减少这个性能上的差异,Swift 实现了一种非常复杂却高效的索引缓存方法,使得这些原本线性时间的操作可以达到
均摊常量时间 (amortized constant time)的性能。
其他基于字符串的 Foundation API
-
NSAttributedString 对应不可变字符串,NSMutableAttributedString 对应可变字符串。和 Swift 标准库中遵守值语义的集合不同,它们都遵守引用语义。
-
NSRange 是一个包含两个整数字段 location 和 length 的结构体:
public struct NSRange { public var location: Int public var length: Int }
字符范围
Character并没有实现 Strideable 协议,而只有实现了这个协议的范围才是 可数 的集合。
CharacterSet
CharacterSet实际上应该被叫做UnicodeScalarSet,因为它确实就是一个表示一系列 Unicode 标量的数据结构体。它完全和 Character 类型不兼容。
Unicode 属性
-
在 Swift5 里,CharacterSet 的部分功能被移植到了 Unicode.Scalar。
("😀" as Unicode.Scalar).properties.isEmoji // true ("∬" as Unicode.Scalar).properties.isMath // true你可以在
Unicode.Scalar.Porperties找到完整的属性列表。
String 和 Character 的内部结构
- 和标准库中的其他集合类型一样,字符串也是一个实现了写时复制的值语义类型。
- 在 Swift 5 里,Swift 原生字符串 (相对于从 Objective-C 接收的字符串) 在内存中是用 UTF-8 格式表示的。
- 对于那些小于16个 (在32位平台上是小于11个) UTF-8 编码单元的小型字符串,作为特别优化,Swift 并不会为其创建专门的存储缓冲区。由于字符串最多只有16字节,这些编码单元可以用内连的方式存储。
- 一个字符现在在内部被表示成长度为 1 的字符串。
字符串字面量
""是字符串字面量。我们可以通过实现 ExpressibleByStringLiteral 协议让你自己的类型支持通过字符串字面量初始化。
字符串插值
字符串插值(String interpolation) 是从 Swift 发布之初就存在的语法特性。他可以让我们在字符串字面量中插入表达式 (例如: "a * b = \(a * b)")。
定制字符串描述
-
通过实现
CustomStringConvertible自定义print和String(describing)输出。通过实现CustomDebugStringConvertible自定义String(reflecting:)的结果。extension SafeHTML: CustomStringConvertible { var description: String { return value } } extension SafeHTML: CustomDebugStringConvertible { var debugDescription: String { return "SafeHTML: \(value)" } }
文本输出流
-
标准库中的
print和dump函数会把文本记录到 标准输出 中。这两个函数的默认实现调用了print(_:to:)和dump(_:to:)。to 参数就是输出的目标,它可以是任何实现了TextOutputStream协议的类型:public func print<Target: TextOutputStream> (_ items: Any..., separator: String = " ", terminator: String = "\n", to output: inout Target) -
我们还可以创建自己的输出流。TextOutputStream 协议只有一个要求,就是接受一个字符串,并将它写到刘中的
write方法。比如,这个输出流将输入写到一个缓冲数组里:struct ArrayStream: TextOutputStream { var buffer: [String] = [] mutating func write(_ string: String) { buffer.append(string) } } var stream = ArrayStream() print("Hello", to: &stream) print("World", to: &stream) stream.buffer // ["","Hello","\n","","World","\n"] -
输出流的源可以是实现了 TextOutputStreamable 协议的任意类型。这个协议需要
write(to:)这个泛型方法,它可以接受满足TextOutputStream的任意类型作为输入,并将 self 写到这个输出流中。struct StdErr: TextOutputStream { mutating func write(_ string: String) { guard !string.isEmpty else { return } // Swift 字符串可以直接传递给那些接受 const char * fputs(string, stderr) } } var standarderror = StdErr() print("oops!", to: &standarderror) -
流还能够持有状态,或者对输出进行变形。除此之外,你也能够将多个流链接起来。
struct ReplacingStream: TextOutputStream, TextOutputStreamable { let toReplace: DictionaryLiteral<String, String> private var output = "" init(replacing toReplace: DictionaryLiteral<String, String>) { self.toReplace = toReplace } mutating func write(_ string: String) { let toWrite = toReplace.reduce(string) { partialResult, pair in partialResult.replacingOccurrences(of: pair.key, with: pair.value) } print(toWrite, terminator: "", to: &output) } func write<Target: TextOutputStream>(to target: inout Target) { output.write(to: &target) } } var replacer = ReplacingStream(replacing: [ "in the cloud": "on someone else's computer" ]) let source = "People find it convenient to store their data in the cloud." print(source, terminator: "", to: &replacer) var output = "" print(replacer, terminator: "", to: &output) output // People find it convenient to store their data on someone else's computer.