UTF-8 编码在编码单个码点为超过一字节长度时,二进制填充时没有加上偏移。
举个例子,ä
是 U+00E4
,二进制就是 11100100
,大于 0x7F
,因此用 UTF-8 编码时占两个字节,模式是 110xxxxx 10xxxxxx
。向
11100100
增加前缀 0
填充为 00011100100
,再填入模式中就得到了 ä
的 UTF-8 编码 11000011 10100100
。
这个过程中存在一个问题:小于等于 0x7F
的码点也能用这种方式编码为双字节。比如 Java 内部用的 MUTF-8 就把全零的空字节编码为
11000000 10000000
以避免编码后的字符串中出现空字节。
UTF-8 标准认为这是非法编码,实践中应当避免。英文一般把这种现象叫做 overlong,一些讨论如下:
https://groups.google.com/g/comp.unix.programmer/c/DYWcy_zQ-NQ
https://www.reddit.com/r/Unicode/comments/1hipf11/why_is_utf8_so_sparse_why_have_overlong_sequences/
https://news.ycombinator.com/item?id=24841211
循其本,我们可以在这里找到该设计的源头:
https://www.cl.cam.ac.uk/~mgk25/ucs/utf-8-history.txt
1 | 1. The 2 byte sequence has 2^11 codes, yet only 2^11-2^7 |
可以看到 overlong 是被有意设计成这样的,原因是两字节的情况下只浪费 $2^7 = 128$ 个组合,占比 $2^7/2^{11} = 6.25%$,相对和绝对数量都 不算多,如果加上偏移看起来像魔法数字,而且 Ken 似乎认为 4 字节及以上的编码是多余的,毕竟 3 字节刚好能容纳 16bit 码点,正好能够表示当年的 Unicode,现在的 BMP 平面中所有码点。
但随着字节数增长,浪费的会越来越多,2 字节时只浪费 128 个,3 字节会浪费 2048 个,4 字节就浪费 65536 个,浪费的大小就是一个 BMP 了……实际也不 算多,目前距离用完已定义的 17 个平面还遥遥无期。
UTF-16 代理对计算时就要加上偏移 0x10000
,这样就不存在浪费,大家用起来也没什么问题。
Overlong 的设计,优点是编解码更简单,但实际上也没简单多少,多的一次偏移计算在当前硬件条件下已经可以忽略不计;缺点是浪费容量,但从常用的 2 到 3 字节编码来看也没浪费多少。优点和缺点都不是很突出,只能说这个设计是一个历史的巧合,也许当年 Ken 想法变一点,现在的 UTF-8 编码就要带上偏移了。