前言
首先要明确的一点是:UTF-16 是将 Unicode 中的字符编码转换为实际存储形态的实现方式。因此,在了解 UTF-16 之前,先要简单认识一下什么是 Unicode。
Unicode
Unicode 是计算机科学领域中的一项业界标准,包括字符集和编码方案等,目的是为了解决传统字符编码方案的局限性,以满足跨语言、跨平台进行文本转换、处理的要求。
Unicode 的编码范围为 0~0x10FFFF,其中 U+0000~U+FFFF 为基本平面(BMP),常见的字符都映射在此平面;U+010000~U+10FFFF 为辅助平面(SMP),剩下的字符都映射在此平面。
下面还有两个概念会贯穿下文:
- 码点:一个字符在 Unicode 编码表中对应的一个编码值即为一个码点,一个码点由一个或多个码元组成
- 码元:即具体编码形式中的最小单位,如 UTF-8 中一个码元为 8 位,UTF-16 中一个码元为 16 位
UTF-16
Unicode 字符集用 0~0x10FFFF 映射字符,而 UTF-16 就是将映射的编码转换为实际存储形态的一种实现方式。UTF 是 Unicode Transfer Format 的缩写,即将 Unicode 转换为某种格式之意。UTF-16 的应用范围十分广泛,Java 和 JavaScript 内部的默认编码方式皆为 UTF-16。
UTF-16 是一种变长编码方式,每个字符编码由 1~2 个码元构成,码元长度为 16 位。其中基本平面的字符为 1 个码元,辅助平面的字符为 2 个码元,也可以说 UTF-16 的编码长度为 2 Byte 或 4 Byte。因此如何判断一个字符是由下一个码元表示还是由下两个码元表示就是下面要关注的一个重要的问题。
编码方式:
在基本平面中有一段空白区间 U+D800~U+DFFF 没有字符的映射关系,这是用来辅助平面字符的,这段区间称为代理区。简单来说,出现了代理区中的码元就意味着字符由下两个码元共同表示,其中前一个码元为高位(H),后一个码元为低位(L)。
设:码点为 U
则:H = ( U - 0x10000 ) / 0x400 + 0xD800,L = ( U - 0x10000 ) % 0x400 + 0xDC00
下面展开介绍算法究竟做了什么
设:码点为 U+1D306
∵ U+1D306 > U+FFFF
∴ 需要由两个码元表示
U+1D306 的二进制表示为:
0001 1101 0011 0000 0110
计算高低位都先减去 0x10000,用二进制表示为:
0001 1101 0011 0000 0110
-
0001 0000 0000 0000 0000
=
0000 1101 0011 0000 0110
即:U+D306
接着,
高位除以 0x400,用二进制表示为:
0000 1101 0011 0000 0110
/
0000 0000 0100 0000 0000
=
0000 0000 0000 0011 0100
再加 0xD800,用二进制表示为:
0000 0000 0000 0011 0100
+
0000 1101 1000 0011 0100
即:U+D834
低位对 0x400 除余,用二进制表示为:
0000 1101 0011 0000 0110
%
0000 0000 0100 0000 0000
=
0000 0000 0011 0000 0110
再加 0xDC00,用二进制表示为:
0000 0000 0011 0000 0110
+
0000 1101 1100 0000 0000
=
0000 1101 1111 0000 0110
即:U+DF06
∴ U+1D306 在内存中的编码为 0xD834 0xDF06,代码中表示为 \uD834\uDF06
我们回过头来对算式过程进行纵向对比不难发现,高低位计算的三个步骤为:
- 去溢出位
- 平分前后 10 位
- 加高低位鉴别
由算式过程反推也可以得出结论:码元高 6 位为 54 的为高位,码元高 6 位为 55 的为低位。
拓展:
- Java 中的
char
类型描述了 UTF-16 编码中的一个码元 - JavaScript 中
String.prototype.length
描述的是字符串编码后的码元个数