Swift 字符
字符(Character)表示人眼看到的一个字符,是字符串的组成单位。
Swift 的 Character 既不是字节,也不是 Unicode 码点,而是一个扩展字素簇,这是它和 C 系语言里的 char 最大的区别。
本篇讲 Character 的含义、扩展字素簇语义、与 String 的互转,以及字面量和类型标注的区别;字符串本身的用法见《Swift 字符串》。
Character 是什么
Character 表示单个扩展字素簇,必须用双引号赋值,并且里面只能放一个字符。
声明时必须写类型标注,否则编译器会把双引号里的内容推断成 String,而不是 Character。
实例
let char2: Character = "牛"
print("char1 的值为 \(char1)")
print("char2 的值为 \(char2)")
运行结果:
char1 的值为 A char2 的值为 牛
注意:Character 不能是空字符串,也不能塞进多个字符。写 let c: Character = "AB" 或 let c: Character = "" 都会报 cannot convert value of type 'String' to specified type 'Character'。
一个字符串由若干 Character 组成,用 for-in 逐个遍历时,遍历出来的每个元素都是 Character。
实例
print(ch)
}
运行结果:
R u n o o b
这里输出的每个 ch 都是 Character,而不是 String,所以能直接参与 Character 的运算和比较。
扩展字素簇语义
Swift 里「一个字符」指的是一个扩展字素簇(extended grapheme cluster),也就是人眼看到的一个字符,而不是底层的字节或码元。
带音标的字母、肤色修饰的 emoji、由多个 emoji 组合成的家庭符号,都算一个 Character。
实例
let precomposed = "\u{00E9}"
// e 加组合音标也是「一个」字符,因为它是同一个扩展字素簇
let combined = "e\u{0301}"
print("precomposed 的 count:\(precomposed.count)")
print("combined 的 count:\(combined.count)")
print("两者相等:\(precomposed == combined)")
// 由四个成员组成的家庭 emoji 同样只算一个字符
let family = "👨‍👩‍👧‍👦"
print("family 的 count:\(family.count)")
// 带肤色修饰的 emoji
let thumbsUp = "👍🏽"
print("thumbsUp 的 count:\(thumbsUp.count)")
运行结果:
precomposed 的 count:1 combined 的 count:1 两者相等:true family 的 count:1 thumbsUp 的 count:1
这些例子的共同点是:底层的 Unicode 标量数量不止一个,但都属于同一个扩展字素簇,所以 count 都是 1。
扩展字素簇的边界规则由 Unicode 标准定义,Swift 直接采用了它,因此不同语言里「一个字符」的直觉在 Swift 中基本都能对上。
Character 与 String 的互转
Character 和 String 之间可以互相转换,最常用的几种写法如下。
| 方向 | 写法 | 说明 |
|---|---|---|
| String 到字符数组 | Array(str) | 得到一个 [Character] |
| String 到单个字符 | for c in str | 每次循环的 c 是 Character |
| Character 到 String | String(c) | 得到一个单字符字符串 |
| 字符数组到 String | String(chars) | 把 [Character] 拼回字符串 |
| 把 Character 追加到字符串 | str.append(c) | str 必须是 var |
实例
// 用 Array 把字符串拆成字符数组
let chars = Array(str)
print(chars)
print(chars.count)
// 取出一个字符,再转回字符串
let first: Character = chars[0]
let asString = String(first)
print(asString)
print(type(of: asString))
运行结果:
["R", "u", "n", "o", "o", "b"] 6 R String
反过来,Character 也可以用 append 追加到字符串末尾。
实例
let exclamation: Character = "!"
text.append(exclamation)
print(text)
运行结果:
Hello !
字符数组拼回字符串时直接写 String(chars);如果中间想加分隔符,需要先把每个 Character 转成 String,再用 joined(separator:),因为 [Character] 本身没有这个方法。
实例
let chars = Array(str)
// 字符数组拼回字符串
print(String(chars))
print(chars.map(String.init).joined(separator: "-"))
运行结果:
Runoob R-u-n-o-o-b
Character 字面量与类型标注
双引号里的内容默认是 String,即使它只有一个字符也一样;只有显式标注类型或用 as Character 转换,才会得到 Character。
实例
let explicit: Character = "C"
let casted = "C" as Character
print(type(of: inferred))
print(type(of: explicit))
print(type(of: casted))
运行结果:
String Character Character
三行代码看起来一样,类型却不同:inferred 是 String,另外两个是 Character。需要 Character 时,优先用类型标注,它比 as 转换更直观。
从变量转换也一样,Character(someString) 可以把一个内容恰好为单字符的 String 转成 Character,但如果字符串为空或长度大于 1,程序会在运行时终止,所以转换前要确认内容。
常见问题
下面是初学者使用 Character 时最常遇到的几个问题。
为什么 "👨👩👧👦".count 是 1
这个家庭 emoji 由四个成员 emoji 和三个零宽连接符组成,底层有 7 个 Unicode 标量、25 个 UTF-8 字节,但它们属于同一个扩展字素簇,人眼看到的就是一个符号,所以 count 是 1。
Character 能不能直接和 String 比较
不能。Character 和 String 之间没有 == 重载,c == s(s 是 String 变量)会报 binary operator '==' cannot be applied to operands of type 'Character' and 'String'。但 c == "A" 是合法的,因为字面量 "A" 会被推断成 Character;要比较 String 变量,先用 String(c) 转换。
实例
let s = "A"
// 字面量会被推断成 Character,可以直接比较
print("c == \"A\":\(c == "A")")
// c == s 则无法编译,Character 和 String 之间没有 == 重载
print("String(c) == s:\(String(c) == s)")
print("c == Character(s):\(c == Character(s))")
运行结果:
c == "A":true String(c) == s:true c == Character(s):true
怎么把字符串拆成字符数组
用 Array(str) 得到一个 [Character],之后就能用数组下标、map、filter 等所有集合操作;只想遍历而不用保存时,直接 for c in str 更省事。
Character 能放空字符串吗
不能,空字符串和多字符字符串都会报 cannot convert value of type 'String' to specified type 'Character'。需要表示「没有字符」时,用可选类型 Character? 并赋 nil。
