Unicode码点、UTF-8与UTF-16详解

从一个普通字母到一个表情符号,你输入的每一个字符都对应着一个Unicode码点——下面来看看这具体是什么意思,以及它是如何最终变成实际存储的字节的。

码点是分配给某个字符的一个编号

Unicode给每一个字符分配了一个唯一的编号,写作U+加上一串十六进制数字,比如字母"A"是U+0041,一个咧嘴笑的表情符号是U+1F600。码点是在概念层面标识字符,和它最终如何存储为字节是两回事。

UTF-8每个字符用1到4个字节表示

UTF-8是一种变长编码:基本的ASCII字符只需要1个字节,而超出这个范围的字符,比如带重音符号的字母、中日韩(CJK)文字或表情符号,则需要2到4个字节。这就是为什么一段文字的字节长度经常比看到的字符数量更长。

UTF-16通常用2个字节,但有时需要4个

UTF-16用一个2字节单元表示大多数常见字符,但对于基本多文种平面之外的字符(包括大多数表情符号),需要用两个2字节单元组成的"代理对"来表示,这也是当代码错误地假设"一个单元等于一个字符"时经常出现bug的地方。

HTML数字字符引用直接使用码点

像😀或😀这样的HTML数字字符引用,是把码点用十进制或十六进制数字的形式编码出来的。当某个字符无法直接输入或显示时,查出它的码点正是把它写成HTML实体的第一步。

相关的字符被归类到有名字的"区块"中

Unicode把码点组织成一个个有名字的区块,比如"基本拉丁文"、"中日韩统一表意文字"或"表情符号",把相关的字符归入连续的编号范围。这在判断一个陌生的码点大致属于哪一类字符时很有用。

为什么同一套字符集会有不止一种编码方式

UTF-8、UTF-16和UTF-32都表示同一套底层的Unicode码点,但在存储效率和处理简便性之间做出了不同的取舍。UTF-8对以英文为主的文本更紧凑,在网络上占主导地位;而UTF-16被固化进了Windows、Java等较早的平台API中,这就是为什么两者都在广泛使用,而没有被彼此完全取代。

为什么一个表情符号有时其实"是"好几个码点

许多现代表情符号,比如带特定肤色的点赞手势,或是家庭表情符号,实际上是用一种叫做零宽连接符的特殊字符把多个码点连接起来的组合序列,在支持的软件里被渲染成一个字形。这就是为什么有些表情符号看起来一样,但底层的码点序列长度却不同。

常见问题

为什么一段文字的字符数有时和字节长度对不上?

因为像UTF-8这样的编码,每个字符所用的字节数是不固定的——一段只包含ASCII字母的文字,字符数和字节数是一致的,但一旦加入带重音符号的字母、中日韩文字或表情符号,字节长度增长的速度就会比看到的字符数量快。

通俗地说,什么是代理对?

这是UTF-16用来表示那些无法容纳进正常2字节单元的字符的一种方式,做法是把两个特殊的2字节单元组合起来当作一个整体使用。如果某段代码错误地把其中每一半当成独立的字符处理,就可能把一个表情符号或罕见字符从中间切断,导致显示错乱。