• 关于Unicode


    最近系统学习了下unicode,总结一下。

    早期的对Unicode的介绍中,很多定义Unicode是16位,以弥补ASCII只能表示8位的不足。但事实上,Unicode最多可以支持32位。其最高位一定是0。然后接下来的7位共有128个数字,可以区分128个group。然后下面的8位,可以区分256个plane(平面)。剩下的16位,用来标示一个平面内的字符,最多有2的16次方即65536个。

    最开始,Unicode只有一个平面,即BMP(Basic Multilingual Plane,基本多文种平面)。2001年3月的unicode3.1,才开始增加其他平面,即辅助平面(Supplementary Planes)。所以如果是上世纪末或本世纪早期已经开始编程工作的人,接触到的资料会说Unicode是一种16位的编码方式。但后来发现65536不足以表示所有字符(连汉字都涵盖不了),所以又搞了多个辅助平面。现在一共有16个辅助平面,加上BMP,总共有17个平面,涵盖了从0x00000到0x10FFFF的所有字符。这些数字,就是对应字符的Unicode编码。因为包括4个Byte,所以称为UCS-4(Universal Character Set,通用字符集)。除此之外,又有一个UCS-2编码。因为UCS-2只有两个bytes,所以只能表示BMP里面的字符,所以这种编码方式已经是obsolete了。

    上面说的UCS-4和UCS-2是Unicode的编码方式,即不考虑计算机如何存储,是字符的一种数字表示。但同一个编码,在计算机中可以有不同的存储方式,这称为实现方式。

    最自然的方式,就是让实现方式跟编码方式一样。我们可以在计算机中使用也32位来保存所有的字符。这就是UTF-32(UCS Transformation Format)实现方式。这里32表示使用32个bit来表示。优点是实现简单,也易读;缺点是浪费空间。绝大多数时间只是用BMP中的字符,使用16位就可以表达。而对于英语用户而言,8位的传统ASCII就基本可以满足。如果都要使用32位,则浪费很多空间。

    为了解决这个问题,又有UTF-16和UTF-8两种实现方式。

    先解释UTF-16。按照名字,UTF-16是使用16位bit来表示字符。这个理解是错的。正确的应该说最少使用16位bit来表示。事实上,对于BMP里的字符,是使用16位bit来表示;对于其他16个辅助plain,则使用32位来表示。因为BMP是最常用的,所以UTF-16基本上可以减少一半的内存使用。现在问题来了,如何判断一个16位bit(下面简称short)是一个独立的字符还是一个字符的前半段或者后半段呢?

    Unicode的设计者在设计编码的时候已经考虑到16位不够用这一点。于是它们在BMP中预留了一段代理区(surrogate),0xD800-0xDFFF。该区间的编码不对应实际字符。如果发现一个short编码落在其中,说明该short编码需要与另外一个short编码共同组成一个真正的unicode字符。具体而言,如果一个short落在0xD800-0xDBFF之间,表示这是高端的16个bit;如果在0xDC00到0xDFFF之间,表示它是低端的16个bit。在表示辅助平面的32个bit中,高端的0xD800-0xDBFF(二进制是0b1101100000000000到0b1101101111111111)的前6个bit必须是0b110110,对于低端的0xDC00到0xDFFF(二进制是0b1101110000000000到0b1101111111111111),其前6个bit必须是0b110111。这12个bit是固定的,而剩下的32-12=20个bit是可以变化的,用来区分不同的编码。这20个bits,其前4位用来表示plain分区,后16位表示一个plain中的字符(因为一个plain可以有2的16次方个字符)。所以正好,UTF-16可以表示16个辅助平面的所有字符。其实并非“正好”,这是unicode设计的结果。

    不知道大家对这种在编码中预留区间以扩展的设计是什么意见?可能大多数人会认为unicode高瞻远瞩,已经预料到16位不够用,所以这么设计。但在我个人看来,这种实际事实上把编码方式和实现方式混淆了。编码方式应该是不考虑具体的实现,而只应该为每个字符定义唯一的编码的时候。但事实上,Unicode的编码却有这么一段不对应任何字符,而完全是出于实现上的考虑,保留了这么一段空间以供扩展。编码时考虑实现方式,编码就不纯粹了。

    另外一个问题是现在的这种编码方式,只是考虑了扩展到16个辅助平面的情况。如果16个辅助plain仍然不够用,那该怎么办呢?我想到的解决办法是仍然采用这种在编码中挖洞的方式。我们可以把最后一个辅助空间保留下来(虽然现在的定义中,它是作为自定义字符的空间)。如果发现32位编码落在这个空间,那说明这个32位是一个完整64位UTF-16编码实现的前一半或者后一半。

    最后一个问题是对于英文为主的文档,传统的8位ASCII就基本够用。但现在对于英文字母也要使用16位来表示,在英文文档中仍然会浪费一半的空间。

    UTF-8可以解决这些问题。不清楚UTF-8和UTF-16哪个在前,但从设计上而言,UTF-8在内存使用,扩展性和设计的纯粹性方面,我觉得都占优。

    类似于UTF-16,UTF-8也是一种变长的编码方式。

    • 出现概率高的字符使用较短的编码,反之出现概率低的则使用较长的编码。不同的字符可能有不同长度的编码。ASCII的128个字符最常用,也处于unicode的最前面,所以用8bit来表示。后续的可以是16bit,24bit和32bit。所以如果一篇文章都是ASCII字母,则相对于UTF-16,可以少用一半的空间。(UTF-16也应用了这种思想,BMP是16位,扩展平面是32位,所以其相对于定长的UTF-32,绝大多数情况可以少用一半空间。)
    • 因为不同长度,类似于UTF-16,我们也需要判断一个byte表示独立的字符,还是某个字符的的多byte编码的一部分。不同于UTF-16挖洞的办法,UTF-8在每个byte的高位使用不同的前缀的方式来区分不同的类型。在前缀的选择上,采用了huffman编码的思想。
      1. 如果是8bit(1个byte),则其首位是0,所以8bit字符的编码即0b0XXXXXXX.
      2. 如果是16bit(2个byte),其高字节的前两位是110,低字节的前两位是10。所以16bit字符的编码是0b110XXXXX 0b10XXXXXX.
      3. 如果是24bit(3个byte),其高字节的前三位是1110,其余字节的前两位是10。所以24字节的编码是0b1110XXXX 0b10XXXXXX 0b10XXXXXX。
      4. 如果是32bit(4个byte),其高字节的前四位是11110,其余字节的前两位是10。所以32字节的编码是0b11110XXX 0b10XXXXXX 0b10XXXXXX 0b10XXXXXX。
        前缀类型可以是0b0,0b10,0b110,0b1110,0b11110。越常出现的,越短;而且一个前缀不会是另一个前缀的前缀,这正是huffman编码的思路。当我随机拿到一个字节,我可以通过前缀bit,很容易的判断它是一个独立字符,还是16位,24位或者32位字符的首字节,或者是后续字节。这里没有必要区分后续字节是16位,24位还是32位的后续,因为遇到之后,只需继续往后走,走到下个合法的首字节即可。
    • 那如何从UCS-4映射到UTF-8呢?对于8bit的编码方式还有7个bit可以变化,所以可以容纳ASCII的0x00到0x7F;16bit的编码方式有11个bit可以变化,可以容纳0x80到0x7FF。其实这个区间只有1919个字符,不到2的11次方即2048个。但为了方便记忆,有的UTF-8编码不对应具体unicode编码;24bit有16个bit可以变化,可以容纳0x800到0xFFFF。于是所有BMP最多24位即可;32bit编码有21bit可以变化,完全可以所有的辅助平面。如果将来有新的辅助平面我们还可以用40bit(首位0b111110XX),48bit(首位0b1111110X),56bit(首位0b11111110)。64bit(首位0b11111111),不仅可以容纳32位的UCS-4编码,即使将来unicode扩展到UCS-6(48位),也完全可以。
    • 当拿到UTF-8编码的文本的任何一个byte,可以根据其前缀知道该byte的类型,然后决定只取出该byte就可以还是需要再取出后续的几个byte。然后将有效bit(非前缀bit)重新组成一个或者几个byte,得到unicode编码。

    总结下UTF-8的优缺点:

    1. 第一个优点,分离了实现和编码。它不需要unicode中预留一些保留空间以扩展。即使unicode是完全连续的,也可以表达;
    2. 第二个优点,最短只要1个字节,而ASCII都可以放在这1个字节中,兼容ASCII,节省空间。
    3. 第三个优点,扩展容易,不仅支持现在所有平面,可以很方便的支持将来的平面。
    4. 缺点是跟unicode编码转换可能较慢,涉及大量位操作。不仅要判断,而且要取出部分和重新拼接。而UTF-16虽然要判断,但不需要取部分和拼接;UTF-32则判断都不需要。

    现实中utf-8和utf-16都很常用。例如java,java的class默认使用UTF-8存储(linux的文件默认都是UTF-8);但运行时,java却采用utf-16编码,这样存储省空间,读入之后,运行也很快。这是我个人的理解,不知道是否真的是设计的初衷。

    但问题是,java诞生之时,unicode只有bmp,所以它直接把char定义为两个字节,String.charAt和String.length都返回的是其第N个char和char的个数。但有了辅助平面,可以两个char才表示一个真正的字符。所以String.charAt可能只返回一个真正字符的高字节或低字节,String.length也仅仅是char的个数,而不是真正的字符数。

    所以Java为String又设计了另一套api,例如String.codePointCount可以获取真正字符数,String.codePointAt可以获取某个位置的真正字符。很多情况下,这套api才是我们真正应该使用的。

  • 相关阅读:
    【VS开发】在VS2010中开发ActiveX控件设置测试容器的方式
    【VS开发】动态添加的ActiveX控件如何响应事件
    【VS开发】动态添加的ActiveX控件如何响应事件
    【VS开发】windows注册ActiveX控件
    【VS开发】windows注册ActiveX控件
    【VS开发】动态创建ActiveX控件
    【VS开发】动态创建ActiveX控件
    【VS开发】win7下让程序默认以管理员身份运行
    【VS开发】win7下让程序默认以管理员身份运行
    【VS开发】使用MFC创建并调用ActiveX控件
  • 原文地址:https://www.cnblogs.com/xichengtie/p/3383890.html
Copyright © 2020-2023  润新知