字符串
Swift 中的 String 是 Character 值的集合,而 Character 是人类在阅读文字时所理解的单个字符,这与该字符由多少个 Unicode 标量组成无关。
Swift里面的String不支持随机访问,不能用类似str[999]来获取字符串的第一千个字符。
当字符拥有可变宽度时,字符串并不知道第 n 个字符到底存储在哪儿,它必须查看这个字符前面的所有字符,才能最终确定对象字符的存储位置,所以这不可能是一个O(1)操作
Unicode,而非固定宽度
ASCII字符串就是由0到127之间的整数组成的序列。可以把这种整数放到一个8比特的字节里。
但是8个比特对于许多语言的编码来说是不够用的。
当固定宽度的编码空间被用完后,有两种选择:
- 增加宽度(当初被定义成2个字节固定宽度的格式,2个字节不够用,4个字节又太低效)
- 切换到可变长的编码(最终选择这种可变长格式)
一些Unicode名词关系
- Swift里说的“单个字符” = Swift里说的1个Character = 1个字位簇
- 1个Unicode字符 = 1个字位簇 = 1个或多个Unicode标量
- 1个Unicode标量 可编码成 1个或多个编码单元
- 1个Unicode标量 大多数情况下可理解成 1个编码点
编码点
- Unicode中最基础的原件叫做编码点,编码点是一个位于Unicode编码空间 (从0到0x10FFFF,也就是十进制的 1,114,111) 中的整数。
- Unicode中的每个字符或其它语系单位或颜文字(emoji)都有1个唯一的编码点。
- 编码点都会写成带有U+前缀的十六进制数,比如欧元符号 -> U+20AC,在Swift里 -> “\u{20AC}” = “€”
编码单元
- Unicode数据,可以用多种不同的编码方式进行编码,其中最普遍使用的是8比特(UTF-8)和16比特(UTF-16)。
- 编码方式中使用的最小实体叫做编码单元,也就是说UTF-8编码单元的宽度是8比特,而UTF-16编码单元的宽度是16比特。
- UTF-8 提供的一个额外的好处就是为使用8比特的ASCII编码提供了向后兼容,正是这个特性,才让 UTF-8接过了ASCII大旗,成为了现如今Web和文件格式中最为流行的编码方式。
- Swift里,UTF-8和UTF-16使用的编码单元的值分别用UInt8和UInt16表示 (它们还有两个别名,分别是Unicode.UTF8.CodeUnit和Unicode.UTF16.CodeUnit)
字位簇和标准等价
合并标记
String
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
//通过比较组成字符串的Unicode标量
single.unicodeScalars.count // 7
double.unicodeScalars.count // 8
//通过比较字符串的utf8
single.utf8.elementsEqual(double.utf8) // false
复制代码
let chars: [Character] = [ "\u{1ECD}\u{300}", // ọ́
"\u{F2}\u{323}", // ọ́
"\u{6F}\u{323}\u{300}", // ọ́
"\u{6F}\u{300}\u{323}" // ọ́ ]
let allEqual = chars.dropFirst().allSatisfy { $0 == chars.first } // true
复制代码
NSString
let nssingle = single as NSString
nssingle.length // 7
let nsdouble = double as NSString
nsdouble.length // 8
nssingle == nsdouble // false
//按照标准等价的方式进行比较两个 NSString,就得使用 NSString.compare(_:) 方法
复制代码
颜文字
Java或者C#里,会认为”?”是两个“字符”长,Swift 则能正确处理这种情况:
let oneEmoji = "?"// U+1F602
oneEmoji.count // 1
复制代码
这里重要的是,字符串是如何呈现在程序中的,而不是它是如何存储在内存中的。
有些颜文字还可以由多个Unicode标量组合而成:
let flags = "????"
flags.count // 2
复制代码
要观察组成字符串的 Unicode 标量,我们可以使用字符串的 unicodeScalars 视图,这里,我
们将标量值格式化为编码点常用的十六进制格式:
fags.unicodeScalars.map {
"U+\(String($0.value, radix: 16, uppercase: true))"
}
// ["U+1F1E7", "U+1F1F7", "U+1F1F3", "U+1F1FF"]
复制代码
把五种肤色修饰符 (比如 ?,或者其他四种肤色修饰符之一) 和一个像是?的基础角色组合起来,就可以得到类似??这样的带有肤色的角色。再一次,Swift 能正确对其处理:
let skinTone = "??" // ? + ?
skinTone.count // 1
复制代码
对于这种表达人群的颜文字,无论是性别还是人数都存在着数不清的组合,为其中每一种组合都单独定义一个编码点非常容易出问题。如果再把这些组合考虑上肤色的维度,让每种情况都有对应的编码点简直就成了一件不可能的事情。
对此,Unicode 的解决方案是把这种复杂的颜
文字表示成一个简单颜文字的序列,序列中的颜文字则通过一个标量值为 U+200D 的不可见零宽连接字符 (zero-width joiner,ZWJ) 连接
ZWJ 的存在,是对操作系统的提示,表明
如果可能的话,把 ZWJ 连接的字符当成一个字形符号 (glyph) 处理。
let family1 = "????"
let family2 = "?\u{200D}?\u{200D}?\u{200D}?"
family1 == family2 // true
family1.count // 1
family2.count // 1
复制代码
字符串和集合
String 是 Character 值的集合
Swift4以后:将两个集合连接的时候,你可能会假设所得到的集合的长度是两个用来连接的集合长度之和。但是对于字符串来说,如果第一个集合的末尾和第二个集合的开头能够形成一个字位簇的话,它们就不再相等。
let flagLetterC = "?"
let flagLetterN = "?"
let flag = flagLetterC + flagLetterN // ??
flag.count // 1
flag.count == flagLetterC.count + flagLetterN.count // false
复制代码
双向索引,而非随机访问
String 并不是一个可以随机访问的集合。就算知道给定字符串中第 n 个字符的位置,也并不会对计算这个字符之前有多少个 Unicode 标量有任何帮助。String 只实现了 BidirectionalCollection,你可以从字符串的头或者尾开始,向后或者向前移动,代码会察看毗邻字符的组合,跳过正确的字节数。不管怎样,你每次只能迭代一个字符。
当你在书写一些字符串处理的代码时,需要将这个性能影响时刻牢记在心。那些需要随机访问
才能维持其性能保证的算法对于Unicode字符串来说并不是一个好的选择
prefix 总是要从头开始工作,然后在字符串上经过所需要的字符个数,在一个线性复杂度的处理中运行另一个线性复杂度的操作,意味着算法复杂度将会是 O(n^2)。
extension String {
var allPrefixes1: [Substring] {
return (0...count).map(prefix)
}
}
let hello = "Hello"
hello.allPrefixes1 // ["", "H", "He", "Hel", "Hell", "Hello"]
复制代码
需要迭代一次字符串,以获取索引的集合indices,map中的下标操作就是O(1)复杂度的,这使得整个算法的复杂度得以保持在 O(n)。
extension String {
var allPrefixes2: [Substring] {
return [""] + indices.map { index in self[...index] }
}
}
let hello = "Hello"
hello.allPrefixes2 // ["", "H", "He", "Hel", "Hell", "Hello"]
复制代码
范围可替换,而非可变
String 还满足 RangeReplaceableCollection 协议
首先找到字符串索引中一个恰当的范围,然后通过调用 replaceSubrange 来完成字符串替换
var greeting = "Hello, world!"
if let comma = greeting.index(of: ",") {
greeting[..<comma] // Hello
greeting.replaceSubrange(comma..., with: " again.")
}
greeting // Hello again.
复制代码
和之前一样,要注意用于替换的字符串有可能与原字符串相邻的字符形成新的字位簇。