乱码是个恼人的问题,Joel在他的书里说过,给你一个字符串,你不知道它的编码,那这个字符串对你是毫无意义的。这一点,对于每一个做过多语言的系统集成的人,都会印象深刻。可能更让人印象深刻的是你亲自去研究这些编码的经历,相信对于不少初学者,这都会是个充满痛苦和混乱但是又必须经历的一个过程。
在《Joel说软件》里,这位老兵用了不小的篇幅来讲解字符编码以及unicode,不过除了上面那句话,其余的细节我都不太有印象了。就像软件里不少细枝末节的东西一样,你不用它,很快就会忘记,下次用到,又要重新学习,只不过学习的周期会越来越短而已。但至少有一个简单的事实,我们是可以记住的,那就是乱码产生的一个简单原因。
通常情况下,当双方使用相同的编码来理解一个字符串,比如我给你GB码,你也用GB码来显示,就不会出现乱码。乱码其实产生于一种误解,比如我给你GB码,你以为它的BIG5码,于是拿到BIG5的代码页里面去查找对应的点阵字模,于是就可能找到一些奇怪的东西,这就是乱码。
---
在区域设置为英文操作系统上,你打开一个文本文件,不管里面到底是什么,只要没有特别说明,系统都会以为它的Ascii码,就象在简体中文和繁体中文的系统上,默认以为是GB码和BIG5码一样。后来有了unicode,声称可以包含人类所有的字符,于是现在流行的操作系统也都支持了unicode,也就是说,不管是哪个国家的文字,只要它保存成文件的时候使用unicode来编码,在任何一个支持unicode的系统上都能正确显示。"任何一个支持unicode的系统"这个词眼很容易让初学者感到疑惑,他们会说,我的WindowXP也支持Unicode啊,但为什么还是会有乱码呢。其实我们碰到的大多数的乱码,都是因为程序设计的问题导致的。比如你用VC写的一个应用程序,里面没有_T,编译的时候没有用Unicode配置,那它永远就是用当前操作系统上默认的编码来理解他所获得字符串。比如GB或者BIG5,而所谓的“WindowXP也是支持Unicode”的说法对你来说没有任何意义,除非你写的应用程序真正使用了unicode。
那为什么这些繁体和简体的操作系统就不能默认使用unicode呢,很多人说是历史的原因吧,毕竟unicode只是几个行业巨头制定的工业标准,而GB和BIG5是有政府背景的。听说当年中国政府强制执行GB18030的时候,还让微软手忙脚乱了几个月,因为之前Bill同志突然想推utf-8。
utf-8为何物。最早发明的Ascii码是单字节,中国人在Ascii基础上发展出GB和BIG5(大概是用在高位填1的方式),用双字节表示汉字,同时也能兼容Ascii码,这就是为什么在中文系统里面可以同时显示汉字和英文的原因。后来发明了的Unicode,不管是中文还是英文都统一使用双字节(当然它定义得很虚,并没有要求物理存储的时候一定要用utf-16)。但此双字节非彼双字节,同一个字,比如“赖”在Unicode里面是0x568D,在GB里面就是0xC0B5。因此Unicode跟原来我们基于Ascii建立起来的GB和BIG5体系是不兼容的,而且用双字节来表示英文字母的确是个不小的浪费。于是就发明了utf-8,一个Unicode字符串用utf-8来表示之后,其中的英文字符重新变成单字节,但中文却变成3个字节。好像有点不公平,这种设计只是为西方人节省了空间。不过在国际化软件设计的Best Practise里还是提倡数据处理用Unicode,数据传输用utf-8。毕竟在XML文档里面,英文字符应该还是比中文多得多。
如果要亲自体验一下这些编码,最方便的工具其实是Notepad。用简体操作系统,打开Notepad,打开紫光拼音输入法,输入几个简体字,这是GB码。在输入法的设置里面切换到繁体输入,就可以输入繁体字,这也还是GB码。其实最早的GB2312里面是没有繁体字的,然后GBK有了繁体,GB18030就把简体,繁体以及各种少数民族的语言都包含进去了。不过跟BIG5相比,也是此繁体非彼繁体,比如“賴”在GB里面就是0xD987,在BIG5里面是0xBFE0。也就是说在简体操作系统里面使用BIG5输入法,就可能会输入乱码。不过这个乱码并不可怕,把它保存下来,把这个txt文件拷贝到繁体操作系统上,就能够正确显示了。为什么会这样的呢,因为在简体操作系统保存的时候,Notepad会默认使用GB码来存,GB和BIG5都是双字节的,所以不会有错,Notepad只管保存,它并不关心里面的乱码还是正常字符。相类似的,把一个DICOM数据包从简体系统传到繁体系统,在PatientName里面想传“中国”两个字,你得先在简体系统上把GB的“中国”变成GBK的“中國”,然后在把GBK的“中國”转换成一组乱码“い瓣”(之所以的乱码,是因为这个网页是用GB处理的,或者准确的说是用GB来输入,然后被CSDN转换成utf-8再传给IE来显示的,而同样的内码被当成BIG5来显示的时候就是“中國”),再传给繁体系统,同时Character Set最好指定为空(似乎只能是这个,因为DICOM没有包含BIG5)。
另外,你也可以用Notepad把刚才输入的GB或者BIG5字符保存成unicode或者utf-8,Notepad除了做编码转换还会在文件头上增加几个字节,用来标识他们使用了unicode(utf-16)或是utf-8。只要没有这个头的,下次打开的时候,Notepad都会按默认的GB或BIG5来处理。Notepad另存文件的时候还有一个选项Unicode big endian,这又是另外一个故事,当年因为无法确定吃鸡蛋的时候要先从小头吃起还是先从大头吃起,小人国里还发生过一场战争。而且至今为止,在字符编码的领域里,这种可笑的战争仍在消无声息的进行。
---
初学者使用Notepad,很容易了解到关于编码的基本知识,但他们很快又会重新陷入混乱。因为现实世界中太多乱七八糟的软件,它们或多或少的实现了一些编码的转换,比如IE,.Net Framework,或者是某个奇怪的XML Parser。不过它们的初衷是好的,希望减少我们的工作量,但最终还是逃不出Joel先生的“漏洞抽象法则”。
微软声称所有的System.String都是unicode,那么在.Net里面很多地方都应该存在自动的编码转换。因为没有看到.Net Framework的代码,只能猜测。比如在简体系统上,当你通过访问TextBox.Text属性时,它就把文本框中的文字认为是GB,并把这个可能是也可能不是GB的文字转换成Unicode,然后返回一个System.String对象。用默认的StreamReader.ReadToEnd()从文本文件读取字符串的时候,做的也是同一件事情,所以如果这个文件不是GB码的,你获得System.String就可能有乱码。或者用StreamWriter.Write()把字符串输出到文件,.NET应该也会做个反向的转换,默认保存成GB文件,除非在实例化StreamReader和StreamWriter的时候你指定了编码。
又如一些Win32 API中定义了char*类型的参数,我们在.Net里面可以用StringBuilder来代替,那么从API获得一个字符串的时候,.Net互操作的机制是否会把GB字符串(比如在简体操作系统上)转换成Unicode,然后传给API的时候也自动把Unicode转换成GB。当然如果是COM互操作,大家都使用unicode,大概就不用转换了。还有就是数据库,我们一般都推荐用nvarchar代替varchar,否则是否也会存在一个unicode到GB的转换。毕竟转换的步骤越多,乱码的风险就越大,跟踪问题的时候就越困难。
在乱码问题面前,机器的确显得十分的愚蠢。因为这种愚蠢,我们可以让一个字符串绕地球一圈,看到在一万个操作系统上都会显示成乱码,只要它没被修改,回到原地时还能正常显示。但这个可能性微乎其微,除非在进入任何一个系统时候,你都欺骗对方这个字符串就是用对方想要的字符集来编码的,请不要再转换了。
---
另外,乱码还可以分好几种。比如你把一个字符串传给一个你并不熟悉的系统,然后看到上面显示的是一串问号,这通常是因为这个系统没有使用或者根本没有安装你所需要的代码页。如果看到的是一些奇怪的字符,通常就是编码不兼容的问题,比如把简体汉字直接放到在繁体操作系统上,本来是GB的编码被当成了BIG5来显示了,幸亏这两个代码页有overlap,要不然你也就只能看到问号了。还有些时候看到同一个字符串前面一段是正常,后面一段是乱码,那可能就是在IO解码的时候(比如网络通信或读写文件),因为缓冲区大小有限的缘故,某个多字节字符被程序从中间截断了,这是一些新手常犯的错误。