Unicode 基础

Unicode 是一种字符编码标准,旨在为全球所有书写系统中的字符分配唯一的编码点。每个字符都被分配了一个码点(Code Point),通常以 U+XXXX 的形式表示。例如:

  • 英文字母 A:U+0041
  • 中文字符 "你":U+4F60
  • Emoji "😊":U+1F60A

然而,Unicode 本身只是一个字符集,实际存储和处理时,需要转换为具体的编码格式,如 UTF-8、UTF-16、UTF-32

UTF 编码

UTF-8

UTF-8 是一种变长编码,字符的长度可以是 1 到 4 个字节

  • ASCII 兼容(1 字节):A (U+0041) → 0x41
  • 多字节字符(2-4 字节):

    • "你" (U+4F60) → 0xE4 0xBD 0xA0(3 字节)
    • "😊" (U+1F60A) → 0xF0 0x9F 0x98 0x8A(4 字节)

UTF-16

UTF-16 采用 2 或 4 字节 进行编码。

  • 基本多语言平面(BMP):2 字节

    • A (U+0041) → 0x0041
    • "你" (U+4F60) → 0x4F60
  • 超出 BMP(辅助平面,Surrogate Pairs):

    • "😊" (U+1F60A) → 0xD83D 0xDE0A(2 个 16 位单元,合计 4 字节)

UTF-32

UTF-32 采用 固定 4 字节,直接存储 Unicode 码点。

  • A (U+0041) → 0x00000041
  • "你" (U+4F60) → 0x00004F60
  • "😊" (U+1F60A) → 0x0001F60A

虽然 UTF-32 访问字符时更简单,但它的存储效率低,因此很少用于文本处理。

为什么 String.length 可能返回不符合预期的值?

大部分编程语言的 String.length 不会计算人类可感知的字符数,而是计算编码单元的数量,这导致如下问题:

void main() {
  String text = "😊";
  print(text.length); // Dart: 2(UTF-16 编码,存储为两个 16 位单元)
}

类似地,在 JavaScript、Java、Kotlin 等 UTF-16 语言中,一个单个 Emoji 可能会被 length 统计为 2。

console.log("😊".length); // JavaScript: 2

为什么 substring 可能会导致问题?

假如我们对包含 Emoji 的字符串使用 substring,但起始或结束索引正好落在 Surrogate Pair 之间,就可能截断字符,导致乱码或异常:

JavaScript 示例

let text = "Hello 😊!";
console.log(text.substring(0, 7)); // "Hello ?"(乱码)

Dart 示例

void main() {
  String text = "Hello 😊!";
  print(text.substring(0, 7)); // 可能崩溃或显示乱码
}

解决方案:可以使用 runes(Unicode 码点遍历)来正确处理字符:

void main() {
  String text = "Hello 😊!";
  print(String.fromCharCodes(text.runes.take(7))); // "Hello 😊"
}

哪些语言避免了这个问题?

Swift 的 String 设计

Swift 使用 Character 作为 Unicode 标准化的感知单元,可以正确处理 Emoji 和组合字符。

let text = "Hello 😊!"
print(text.count) // 8(正确)

Swift 也保证 String 操作不会截断 Grapheme Cluster(如 Emoji 组合):

let index = text.index(text.startIndex, offsetBy: 7)
print(text[..<index]) // "Hello 😊"

结论

  1. length 不可靠:许多语言(Dart、JavaScript、Kotlin)会把 Emoji 视为 2 或 3 个字符,而不是 1。
  2. substring 可能截断字符,导致乱码或异常。
  3. 解决方案

    • 遍历 runescodePoints 以正确处理 Unicode 码点。
    • 使用 Swift 这样的语言,其 String API 内部保证不会截断 Grapheme Cluster

Emoji 在 Unicode 中的存储方式使得很多编程语言需要特殊处理,否则可能会导致文本显示错误或逻辑异常。理解 Unicode 及其不同的编码方式,有助于开发者在各种语言中正确处理多字符单元的文本。