在深入研究 字符集编码, 简称 编码 之前, 我们先引入一个概念: 编号(code), 引入它是为了更好地与 编码(encode) 相区分.
如果你对 Unicode 有深入了解, 你也许已经意识到了 Unicode 中 码点(code point) 扮演的正是 编号 的角色. 类似的还有 GB 系列中所谓的 区位码.
其实叫什么并不重要, 爱咋咋地, 我并不关心. 但乱叫容易叫混了, 比如把 码点 也叫成 Unicode 编码, 这里先把这些归入到 编号 概念. 为区别起见, 用黑色加粗的 编码 特指 字符集编码.
到了后面你甚至会为 字符集编码 的边界在哪而困惑, 为它的准确定义而纠结, 不过到那时你已经属于"难得糊涂"了, 编号 这一概念你也可以把它丢到爪哇国去了.
编号是什么?
编号 可以看作是 字符 与 编码 中间的一个抽象层, 过渡层:
- 广义上说, 编号 也可看成是某种编码;
- 狭义上说, 编码 也可视作为某种编号.
图: 字符<-->编号<-->编码
编号 与 编码 的主要区别在于 编号 不涉及具体使用多少字节来表示, 是用定长还是变长方案等细节问题.
编号 仅仅是一个 抽象 的概念, 是把字符数字化的一个过程.
在进一步比较编号与编码之前, 我们先看看编号是如何来的.
字符集通常是带编号的有序集合
- 数学意义上的 集合(Set): 一组不重复的, 无序的 的元素.
- 一般意义上的 字符集: 不重复的, 带 编号 的 有序的 的字符集合.
图: 数学意义上的字符集对比实际的字符集
编号是如何来的?
字符被整理出来之后往往数量众多, 通常是置于表格之中. 出于统计方面的原因, 人们通常用一个数字来编号. 表格本身就是一种有序的暗含了编号的形式, 哪怕你没有明确地为其编号, 人们拿到这个表格也会说: "嘿, 第一个字符是 XXX!"这里的一不就是编号吗?
可以把整理的工作视作是由一群完全不懂计算机的语言文字学家来完成的, 他们甚至连 字节 是什么都没听说过.
所谓 编号 是抽象的就是说它仅仅是一个数字而已.
怎样才算不重复?
这里不打算去讨论哲学意义上的等同, 你可能会碰到有些字符彼此间长得非常像, 在有了编号后, 我们可以简单地说, 只要是编号不同的两个字符就是不重复的.
图: 一些很相似的字符, 图片来自 http://wiki.secondlife.com/wiki/Unicode_In_5_Minutes
这里的 U+[XX]XXXX
是码点的表示形式, X 代表一个十六制数字, 可以有 4-6 位, 不足 4 位前补 0 补足 4 位, 超过则按是几位就是几位. 具体范围是 U+0000~U+10FFFF.
具体例子有:
- U+0048
- U+00E6
- U+4F60
- U+1D11E(注:这是个五位的码点)
注意不要将它与 UTF-16 的编码搞混了, 尤其是那些 4 位的码点, 虽然很相似, 但一个是 编号(code), 一个是 编码(encode), 处在不同的概念层次.
关于码点及 Unicode 的更详细介绍, 可见 字符集与编码(四)--Unicode
我们可以看到至少有三个码点上的 a 是非常像的. 其中的 U+FF41 中所谓的 FULLWIDTH(全宽)其实就是全角, 也即两个半角宽度, 我想我们对此都很熟悉.
当初 GB2312 出来时, 仗着自己编码空间大, 把 ASCII 里那些字母符号之类的又重复弄出一套所谓的全角版本来, 后来 Unicode 又把这些又再收罗了过去.
编号一定是一个数字吗?
不一定! 它也可以是 数字对, 或者你叫它复数, 二元数啥的, 随便你. 但只要它是离散可数量子化的, 它自然也可以转换成唯一的一个数字.
参见前面图中的二维区位编号, 我们用数字对 (1, 1) 编号 h 这个字符. (1, 1) 可以简单转换成 11, 然后可以进一步映射到从 0 或者 1 开始的编号.
编号是连续的吗?
有序不意味着连续. 这里需要说明的一点是, 从前面的叙述来看, 编号早于编码, 但实际情况是人们通常是一起考虑这两者的, 编号反过来会受到编码考虑的影响, 这样做只是为了让从编号到编码的 映射 或者叫 转换 更加方便. 这些影响包括:
- 如果按日常习惯, 编号通常应该从 1 开始, 受编码影响, 编号也从 0 开始.
- 编号写成十进制是更自然的方式, 受编码影响, 编号通常也以十六进制形式来书写, 并写成固定的位数, 不够时就在前面填充 0.
比如把 48 写成 0048;又比如 U+1D11E 就是一个一个五位的编号.
- 为了以后的扩展方便, 编码常常会跳过某些码位, 甚至会保留大片的区域未定义或作保留用途.
比如 Unicode 有所谓的 代理区(surrogate area), 后续我们会进一步了解. 编号因此也跳过这些. (其实到了后面你会发现, 究竟谁影响谁还真不好说!不过等你明白之时这些已经不重要了. . . )
总之一句话就是让映射规则尽可能简单.
图: 编号最终与编码几乎一样的一种可能情形, 为简单起见, 使用十进制.
编码与编号的区别?
你可能会说, 那这样它们还有什么区别? 你的确可以把编号也说成是编码.
但事情并不总是这样, 这种相似性确实迷惑了很多人, 特别是 Unicode, 很多人把 码点 说成是 Unicode 编码, 这种说法本身并没有错, 这取决于你如何定义编码.
但他们是否意识到码点仅仅是一种抽象的编码呢? 为了区别, Unicode 把最终的具体编码称为 UTF(Unicode Transformation Format), 即所谓的 Unicode 转换格式.
所谓转换, 其实就是把抽象的数字映射到具体的, 最终的 编码 上来.
Unicode 编码的两个层面
可以认为 Unicode 编码存在两个层面, 字符层面下, 先是 抽象编码层面, 然后再到 具体编码层面, 如下图所示:
抽象编码层面
把一个字符编码到一个数字.
不涉及每个数字用几个字节表示, 是用定长还是变长表示等具体细节.
具体编码层面
即 UTF, 把抽象编码层面的数字(码点) 再编码 成最终的存储形式.
需要明确是用定长还是变长;定长的话定几个字节;用变长的话有哪几种字节长度, 相互间如何区分等等.
注: 在上一层面, 字符与数字已经实现一一对应, 对数字编码实质就是对字符编码.
一个具体事例
这里所谓抽象与具体, 以 U+0061(ASCII 字母 a)为例, 十六进制的 0061 也就是十进制的 97. 所谓抽象, 也即是用 97 这个数字表示 a; 所谓具体, 就是在计算机的底层到底怎么表示 97 的问题.
即便是表示一个整数, 你也面临着到底是用 byte, short, int 还是 long 来表示的问题, 这就是具体, 不同的表示使用的字节数是不一样的, 也决定了所能表示的数的范围.
更具体到编码, 你还面临是用定长还是变长等抉择.
以 UTF-32 为例, 本质上与一个四字节的 unsigned int(无符号整型)没什么区别, 可以视作为一个 unsigned int 的数组;
而 UTF-8 则是变长的, 无法对应某种整型数组.
UTF-16 同样是变长的, 不过对于常用字符都是两字节, 只有那些非常偏门的字符才是四字节, 这也导致不少人误以为它是定长两字节的.
什么是编码? (广义)
编码是一个非常宽泛的概念! 虽然我们前面一直用 编码 特指 字符集编码, 但这只是一种狭义的理解, 广义的理解则有很多:
- 文字是对声音的编码
- 照相机, 摄像机把光信号编码成图像及视频
- 我们还经常能看到条形码, 二维码, 这些都是编码
在<<编码: 隐匿在计算机软硬件背后的语言>>(Code: The Hidden Language of Computer Hardware and Software)一书中, 参见豆瓣读书, 作者提到了莫尔斯电码(Morse Code)
以及布莱叶盲文(Braille Code), 这些都是编码的例子.
在<<信息简史>>( The Information: A History, A Theory, A Flood)一书中, 参见豆瓣读书, 作者提到了一个有趣的"会说话的鼓"的故事, 非洲的一些部落成员之间可以用鼓声来交流非常复杂的讯息, 在这里就是用鼓声来编码信息.
电影<<修女传>>中有这样的情形, 当修女们还坐着船在刚果河上行进时, 船上的鼓手们就提前用鼓声告诉远方的目的地村庄, "来了一位美丽的修女"(由奥黛丽·赫本(Audrey Hepburn)主演). 声音的速度毕竟比船快!
字符集编码再审视
回到我们的字符集的例子, 虽然我们倾向于认为 编码 就是指最终存储的形式, 比如写入文件时或者放在内存时, 又或者是在网络传输的过程中.
但如果我们要说, 字符集编码 这一概念也可以包含抽象层面的编码, 那么这样一种说法也并无不妥, 只要你能准确区分这两个层面, 你怎么去看待它们都是可以的, 还是前面那句话, 这取决于你如何定义编码, 比如我们可以说 GBK 中的区位码难道不是字符集编码吗? 这就取决于你如何看待 GBK 编码这一概念了, 是狭义地去看待还是广义地去看待.
我不想为字符集编码的准确定义去争论, 在我看来, 当你说编码时, 只要你自己清楚说的是哪个层面就 OK 了, 我们在前面引入了编号, 目的是为在尚未澄清之前作更好的区分, 如果你现在已经清楚了, 就可以把编号丢掉了.
事实上, 当人们说到 Unicode 编码时, 更常是指它的抽象编码层, 即 码点 这一层面, 实际上这才是 Unicode 的核心所在.
Unicode 的核心就是为每个字符提供唯一一个数字编号.
Unicode provides a unique number for every character.
Unicode 编号及编码的一个具体事例
让我们来看一个更加具体的示意图:
关于 码点 如何具体转换成各种 编码, 这个在后面再作讨论. 从图上我们可以初步得出一些结论. 比如
- 三种编码均能表示从 U+0000 ~ U+10FFFF 的所有字符.
- UTF-8 与 UTF-16 都是 变长 编码, UTF-32 则是 定长 编码.
- 码点到 UTF-32 的转换最简单, 就是在前面垫 0 垫够 4 字节就行了.
- 码点到 UTF-8 的转换, 除了最小那个在数值上一样外, 其它三个完全看不出两者的关系.
- 码点到 UTF-16 的转换则是最微妙的, 可以看出前三个字符 UTF-16 与码点是完全一致的, 但那个大码点(准确地说是超过了 U+FFFF 的码点)则有了很大的变化, 长度变成了四字节, 值也变得很不一样了.
关于以上三种编码方案的定变长与字节数总结:
- UTF-8: 变长, 1-4 字节;
- UTF-16: 变长, 2 或 4 字节;
- UTF-32: 定长, 4 字节.
关于 UTF-16 的误解是很多的, 部分可能由于它的名字上带了个 16, 让人误以为它是 16 位定长的两字节编码. 但正像 UTF-8 并不是仅仅是 8 位一样, UTF-16 也不仅仅是 16 位.
事实上, UTF-16 的前身 UCS-2 确实是 16 位定长的编码, 它跟码点在形式上就是完全一样了, 实际我很怀疑那时候压根就没码点这一说法, 那时人们甚至也不说 UCS-2, 直接就叫 Unicode!
时至今天, 你依然可以在不少地方看到把 UTF-16 写成 Unicode 的, 然后与 UTF-8 并排在一起, 显得不伦不类的, 当然了, 这是有历史原因的.
UTF-16 为何变成变长了?
简单地说, 字符扩充了, 目前码点的范围是 U+0000~U+10FFFF. U+10FFFF 是多大呢? 大概是 111 万.
按 Unicode 官方的说法, 就这样了, 以后也不扩充了, 一百多万足够用了, 目前也只定义了 10 万多个字符左右. (截至 Unicode 7.0 标准, 随着新标准的颁布, 收录的字符会不断增多. )
而 16 位定长的话, 撑死了也就 6 万多, 所以不变就不行了. 在后面的篇章中, 将进一步分析定长, 变长的问题.