2025-07-19 | 研究与探索 | UNLOCK

UTF-8 编码的一个瑕疵

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://stackoverflow.com/questions/69318921/why-does-the-utf-8-encoding-have-the-concept-of-overlongs

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
2
3
4
5
6
7
8
1. The 2 byte sequence has 2^11 codes, yet only 2^11-2^7
are allowed. The codes in the range 0-7f are illegal.
I think this is preferable to a pile of magic additive
constants for no real benefit. Similar comment applies
to all of the longer sequences.

2. The 4, 5, and 6 byte sequences are only there for
political reasons. I would prefer to delete these.

可以看到 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 编码就要带上偏移了。