【字符编码系列之五】Unicode实现之UTF-16详解

时间:2023-01-10 22:17:42

历史

Unicode标准规定U+D800~U+DFFF的值不对应于任何字符。之所以这样做,是为了UTF-16。


一开始的时候,Unicode是被设计成用固定的16位来表示代码点的。那时也被称作UCS-2时代。很自然,这些代码点上的值就直接不经过任何变换,直接作为UTF-16的编码了。后来,人们发现两个字节不足以表达世界上所有的书写系统,所以Unicode又被扩充为了4个字节,但是实际上只用了21位。也就是0x0000~0x10FFFF。此时,进入了UCS-4时代。


UTF-16

这时,如果还想继续使用UTF-16表达除了BMP平面之外的字符,就要进行一些改进了。之就引入了代理对的概念。引入代理对,是为了表示BMP平面之外的字符,也就是0x10000~0x10FFFF上的字符。对于表示BMP平面上的字符,UTF-16还是使用2个字节,对于表示BMP平面之外的字符,则使用4个字节。顾名思义,UTF-16是以16位递增的,2字节,4字节。

1.对于BMP平面上的字符,其代码点的值就是其UTF-16的值,此时字符占用2个字节;
2.对于BMP平面之外的字符,要用代理对儿,进行转换即可,此时字符占用4个字节。


代理对

下面我们来看一下什么是代理对。这就要用到前面讲到的保留区U+D800~U+DFFF。
对于BMP平面之外的字符,我们用4个字节来表示,对于高位的2个字节,我们记为W1,对于低位的2个字节,我们记为W2,我们把W1初始化为0xD800,把W2初始化为0xDC00。展开成二进制我们看得更清楚。

0xD800
1101 1000 0000 0000


0xDC00
1101 1100 0000 0000

可以看到,对于W1和W2,各自有10位可以*的编码使用。我把这些位都标记成了绿色。
对于在BMP平面之外的字符,我们记为U,肯定有U>=0x10000且U<=10FFFF,我们令U' = U - 0x10000,所以U'的范围就在0x00000~0xFFFFF之间了。也就是说U'可以用20位来表示。
之后,我们把U'中高字节的10位赋值给W1中的低10位,把U'中低字节的10位赋值给W2中的低10位,也就是绿色的部分。

用图形生动地表示如下:
U' = yyyyyyyyyyxxxxxxxxxx
W1 = 1101 10yy yyyy yyyy
W2 = 1101 11xx xxxx xxxx

由上面看来,一旦UTF-16中字节开头是110110或110111打头的,肯定就是代理对儿了。
易知W1的范围是0xD800~0xDBFF,W2的范围是0xDC00~0xDFFF
W1和W2的范围合起来就是0xD800~0XDFFF,也就是上面提到的保留区。


UTF-16的解码

UTF-16的解码过程正好相反:我们读取两个字节,记为W,这个W可能是W1,也可能是W2.

1.首先,if W < 0xD800 或者 W > 0xDFFF,那么W的值就是字符的代码点。
2.否则,如果 0xD800 <= W <= 0xDBFF,那么继续,否则,我们读到的就是W2,表明这个编码不合法,因为没有W1.
3.我们知道了W在W1的范围内,如果后面不够两个字节的数据,则也出错。否则,就再往后读两个字节,并判断是否在W2的范围区间内。如果不在,也出错。如果在,进行第4步。
4.把W1的低10位取出来,赋给U'的高10位,并把W2的低10位取出来,赋给U'的低10位。
5.U'+= 0x10000
此时,U'的值就是对应字符的代码点。


LE BE BOM

至于大端小端和BOM,本系列的前几篇文章中已经讲的很清楚了。不过还是要注意LE和BE,以及BOM的问题。

引用*中的一张图,来作为UTF-16的结尾。

【字符编码系列之五】Unicode实现之UTF-16详解

我们可以看到,对于BMP中的字符,UTF-16和代码点是完全一致的。这也导致了它不兼容ASCII。不过,对于所有BMP中的CJKV字符,现在终于又成2个字节了。比UTF8少了一个字节。