编程中,字符串一般使用 Unicode。但是,绝大部分人并不了解 Unicode,常常犯一些低级错误。本文列出一些问题,都常在实际情况下遇到。不讲过于技术性或理论性的东西,因为我不懂。
本文也为我自己备忘。和绝大部分人一样,我也不了解 Unicode,本文可能有错误。
资料
这个术语表很好用,简洁明了:Glossary。或者直接看标准。
字符
一般认知的字符常常是 grapheme cluster。Unicode 中的字符一般指 encoded character,即从 abstract character 到一个或多个 code points 的映射(例如带附加符号的字母可以映射到多个 code point)。至于 abstract character,我搞不懂到底是啥。技术上常说的字符一般是 code point。而编程语言的字符类型可能有如下几种:
- Code unit(码元)。通常是 UTF-16 的,也就是十六位无符号整数。也有 UTF-8 的,八位。Java 用这种。
- Code point(码点)。0 到 10FFFF 的整数。Haskell 用这种。
- Scalar value。Code point 去除代理字符。也就是 0 到 D7FF 或 E000 到 10FFFF。Rust 用这种。
- Grapheme cluster(字簇)。据说 Swift 用这种。
Code unit 在访问上比较容易,而 scalar value 和 code point 则可以有其属性。
字符串
标准的字符串是 code unit 序列,用于表示 scalar value 序列。
字符串类型通常使用 UTF-16 或 UTF-8。许多使用 UTF-16 的语言(如 JavaScript)允许字符串格式错误,也就等同于十六位无符号整数序列(非正式名称 WTF-16)。
字符串操作
正确的字符串连接后依然正确,切分则不一定。WTF-16 随便怎么弄都依然是 WTF-16。
正确的字符串,首尾切除若干 code unit 后,若把一个字符切两半就会错误。但是,正确与否一定可以检测出来,即便错误,也一定可以剔除错误部分,不会连累其他字符,也不会多出莫名其妙的字符。不过 WTF-16 不行。
字符串匹配(子串查找)可以直接在 code unit 层面进行,如果字符串格式正确,结果也正确(若要正规化,则应先正规化)。
字符串的相等比较,应当正规化:NFC
、NFD
、NFKC
、NFKD
。否则,附加字符等情况可能会让相同的 abstract character 序列有着不同的 code point 序列。
正规化是幂等的。
正规化是上下文相关的,转换后连接不等于连接后转换。
字符操作
通过字符串的下标访问 scalar value 或者 code point,通常是线性复杂度的。如果复杂度很低,一般说明访问的其实是 code unit。
一般不需要通过字符串的下标访问字符(例如 scalar value)。如果用到了,一般说明做法不好。建议使用字符串匹配,或者正则表达式。
要获取字符串中的字符,建议获取 grapheme cluster。这通常都是最佳方案,例如在翻转字符串时。用 scalar value 或 code point 会在附加符号等地方出问题,但好歹还是能用的。不要用 code unit 直接操作字符,你很可能会得到字符的一部分,啥都干不了。
字符属性
绝对不要自己处理字符的性质,除非你只用 ASCII。Unicode 自带了很多字符的属性,用于判断字符性质。JavaScript 的正则表达式可以用 \p
匹配属性,请记得在正则后加上 u
标志。例如:
- 匹配汉字:
\p{Script=Han}
或\p{Ideographic}
- 匹配大小写:
\p{Uppercase}
和\p{Lowercase}
(若要判断字符串是否大小写,不应用此方法) - 匹配 Unicode 标识符:
\p{XID_Start}\p{XID_Continue}*
(注意,XID_Start
不含下划线)
大小写转换
大小写转换有可能增加长度(例如德语「ß」的大写是「SS」)。
大小写转换是上下文相关的,转换后连接不等于连接后转换(例如希腊字母 ∑ 的小写是 σ 或 ς)。
大小写转换可以是本地化的,不同地区转换结果可以不同(似乎只用于土耳其语的 Iı、İi)。
转过去再转回来,不等于原来的(废话)。转过去再转回来再转过去,不等于直接转过去(上面哪个德语的例子就是)。
判断是否大、小写,应先进行 NFD
规范化,然后转为大、小写,判断有无变化。
大小写无关形式应当进行大小写折叠,使用专门的 toCasefold
方法,不要转成大写或小写。
如果要做大小写无关的、规范化的相等比较,NFC
或 NFD
规范化应当在 toCasefold
后进行,然后再比较。例如 NFD(toCasefold(x))
。该操作是幂等的。
其他
Unicode 博大精深,我大多都不了解,在此列举一些 Unicode 包含的内容:
- 断词算法,以及零宽空格和零宽连接符等
- 双向算法(从左往右或从右往左),以及相关字符(很多人喜欢在用户名里用之)
- 汉字的基本信息(UniHan),如读音、结构等
2025-07-28 补充:修复了一些错误,添加了一些内容。此外,Unicode 16 终于能在网页看了!终于不用等巨大的 PDF 打开半天了!喜大普奔!!!