Unicode 常见问题

创建于 12/6/2023,最后修改于 7/29/2025

实际会遇到的坑

编程中,字符串一般使用 Unicode。但是,绝大部分人并不了解 Unicode,常常犯一些低级错误。本文列出一些问题,都常在实际情况下遇到。不讲过于技术性或理论性的东西,因为我不懂。

本文也为我自己备忘。和绝大部分人一样,我也不了解 Unicode,本文可能有错误。

资料

这个术语表很好用,简洁明了:Glossary。或者直接看标准

字符

一般认知的字符常常是 grapheme cluster。Unicode 中的字符一般指 encoded character,即从 abstract character 到一个或多个 code points 的映射(例如带附加符号的字母可以映射到多个 code point)。至于 abstract character,我搞不懂到底是啥。技术上常说的字符一般是 code point。而编程语言的字符类型可能有如下几种:

  1. Code unit(码元)。通常是 UTF-16 的,也就是十六位无符号整数。也有 UTF-8 的,八位。Java 用这种。
  2. Code point(码点)。0 到 10FFFF 的整数。Haskell 用这种。
  3. Scalar valueCode point 去除代理字符。也就是 0 到 D7FF 或 E000 到 10FFFF。Rust 用这种。
  4. Grapheme cluster(字簇)。据说 Swift 用这种。

Code unit 在访问上比较容易,而 scalar valuecode point 则可以有其属性。

字符串

标准的字符串是 code unit 序列,用于表示 scalar value 序列。

字符串类型通常使用 UTF-16UTF-8。许多使用 UTF-16 的语言(如 JavaScript)允许字符串格式错误,也就等同于十六位无符号整数序列(非正式名称 WTF-16)。

字符串操作

正确的字符串连接后依然正确,切分则不一定。WTF-16 随便怎么弄都依然是 WTF-16

正确的字符串,首尾切除若干 code unit 后,若把一个字符切两半就会错误。但是,正确与否一定可以检测出来,即便错误,也一定可以剔除错误部分,不会连累其他字符,也不会多出莫名其妙的字符。不过 WTF-16 不行。

字符串匹配(子串查找)可以直接在 code unit 层面进行,如果字符串格式正确,结果也正确(若要正规化,则应先正规化)。

字符串的相等比较,应当正规化:NFCNFDNFKCNFKD。否则,附加字符等情况可能会让相同的 abstract character 序列有着不同的 code point 序列。

正规化是幂等的。

正规化是上下文相关的,转换后连接不等于连接后转换。

字符操作

通过字符串的下标访问 scalar value 或者 code point,通常是线性复杂度的。如果复杂度很低,一般说明访问的其实是 code unit

一般不需要通过字符串的下标访问字符(例如 scalar value)。如果用到了,一般说明做法不好。建议使用字符串匹配,或者正则表达式。

要获取字符串中的字符,建议获取 grapheme cluster。这通常都是最佳方案,例如在翻转字符串时。用 scalar valuecode 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 方法,不要转成大写或小写。

如果要做大小写无关的、规范化的相等比较,NFCNFD 规范化应当在 toCasefold 后进行,然后再比较。例如 NFD(toCasefold(x))。该操作是幂等的。

其他

Unicode 博大精深,我大多都不了解,在此列举一些 Unicode 包含的内容:

  • 断词算法,以及零宽空格和零宽连接符等
  • 双向算法(从左往右或从右往左),以及相关字符(很多人喜欢在用户名里用之)
  • 汉字的基本信息(UniHan),如读音、结构等

2025-07-28 补充:修复了一些错误,添加了一些内容。此外,Unicode 16 终于能在网页看了!终于不用等巨大的 PDF 打开半天了!喜大普奔!!!