Unicode 常见问题

创建于 12/6/2023

实际会遇到的坑

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

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

资料

这个术语表很好用,简洁明了:Glossary

或者直接看标准(文件很大!)。

字符

字符类型常常不是一般认知的字符。

以下几种通常用作字符类型:

  1. Code unit。通常是 UTF-16 的,也就是十六位无符号整数。也有 UTF-8 的,八位。
  2. Code point。0 到 10FFFF 的整数。
  3. Scalar valueCode point 去除代理字符,也就是 0 到 D7FF,或 E000 到 10FFFF。

Java 使用第一种。Haskell 使用第二种。Rust 使用第三种。
偶尔还有用 grapheme cluster 的,那更符合一般认知。

字符串

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

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

字符串操作

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

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

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

字符串的相等比较,应当正规化:NFCNFDNFKCNFKD。正规化是上下文相关的,转换后连接不等于连接后转换。

字符操作

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

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

要获取字符串中的字符,建议获取 grapheme cluster。这通常都是最佳方案,例如在翻转字符串时。

绝对不要自己处理字符的性质,包括但不限于大小写的转换和判断。例如语法解析,可以用 Xid_Start 之类的来解析标识符。

大小写转换

大小写转换会改变长度。

大小写转换是上下文相关的,转换后连接不等于连接后转换。

大小写转换可以是本地化的,不同地区转换结果可以不同。

转过去再转回来,不等于原来的(废话)。转过去再转回来再转过去,不等于直接转过去。

大小写无关形式应当使用专门的 toCasefold 方法,不要转成大写或小写。

如果要规范化,NFCNFD 规范化应当在 toCasefold 前后各进行一遍,进而是幂等的。例如 NFD(toCasefold(NFD(x)))