Unicode是一个支持世界上绝大多数语言的字符系统。支持Unicode的编程语言更容易实现全球化(internationalization)和进行本地化(localization)。那么支持Unicode需要编程语言具有什么样的特性呢,让我们来以Python 3和Go语言为例子做个探究。
以UTF-8作为默认编码
Python 3和Go语言默认都使用UTF-8编码,主要体现在下面这几个方面:
- 假设源代码保存在以UTF-8编码的文件中
- 字符串编解码的时候,默认使用UTF-8编码
事实上,对于源代码以UTF-8格式保存这个问题,Go语言不仅是默认,而且是强制。如果编译的时候发现源代码不是以UTF-8编码,Go会直接报错,出现类似下面的信息:
./test.go:6:15: invalid UTF-8 encoding
Python虽然也默认源代码是UTF-8编码的,但是它比较宽容一点,支持其他8位的编码,例如ISO-8859-1,具体参考PEP 263 – Defining Python Source Code Encodings。
让字符串是字符串,字节数组是字节数组
Python 3针对Python 2的重大改变之一就是全面引入的对Unicode的支持。字符串是用来表示Unicode文本,字节数组是用来表示二进制数据。在Python 2里面不管是字符串还是字节数据,都是用str
类型表示。而这两种数据类型在Python 3里面是严格区分对待的,字符串用str
表示,字节数组用bytes
表示。str
类型和bytes
类型不能混用,否则会出TypeError
。
在Python 3中,想要把表示Unicode文本的str
类型转化成表示二进制数据的bytes
类型,必须通过某种编码的方式进行,也就是要调用str.encode()
。反之,从bytes
到str
的转化需要通过解码过程,也就是要调用byetes.decode()
。
具体的例子:
>> text = "你好" #这是一个unicode字符串
>> text.encode()
b'\xe4\xbd\xa0\xe5\xa5\xbd' #默认以UTF-8编码,每个汉字三字节,共六字节
>> b'\xe4\xbd\xa0\xe5\xa5\xbd'.decode()
'你好' #得到原有的字符串
和Python 3类似,Go语言也区分对待文本和数据。Go语言中二进制数据用字节数组[]byte
表示,文本用string
类型表示,这两种类型在转化的时候会有隐含的编解码过程,如下面例子:
s1 := string(127)
fmt.Printf("%#x %q\n", s1, s1)
s2 := string(128)
fmt.Printf("%#x %q\n", s2, s2)
上面的程序输出结果是:
0x7f "\u007f"
0xc280 "\u0080"
为啥127
转化string后,值是0x7f
;而128
转为string后,值是0xc280
呢?这是因为Go语言使用UTF-8对128(也就是U+0080)这个Unicode码位进行了编码。根据UTF-8 & Unicode, what’s with 0xC0 and 0x80?这篇StackOverflow帖子的介绍,U+0080的UTF-8编码方式如下:
UTF-8
Range Encoding Binary value
----------------- -------- --------------------------
U+000080-U+0007ff 110yyyxx 00000yyy xxxxxxxx
10xxxxxx
U+0080的二进制是00000000 10000000
,按上面的转化方式,就变成了11000010 10000000
,得出来的十六进制值就是c280
。
在类型和控制语句上对Unicode支持
在Python中,Int
类型是无级整型,可以用来表示任意大的整数。所以Int
可以直接用来表示Unicode的码位。此外,有两个相应的函数chr
和ord
来帮助进行码位和字符之间的转化:
>>> ord('汉')
27721
>>> ord('字')
23383
# 以上代码给出的“汉字”这两个字符的Unicode码位,分别是27721和23383。
# 可以用chr把码位转化为相应的字符:
>>> chr(27721)
'汉'
>>> chr(23383)
'字'
#也可以直接在字符串中循环读取单个字符
>>> for x in '汉字': print(x)
汉
字
Go语言的整型和C语言类似,都是有长度的,所以Go专门用一个rune
类型来表示Unicode码位。实际上,rune
就是一个32位的无符号整型。Go的for循环也支持直接读取单个字符,如下所示:
for _, x := range "汉字" {
fmt.Printf("%x %q\n", x, x)
}
上面程序的输出是:
6c49 '汉'
5b57 '字'
String类型的内部实现
既然string类型可以盛放Unicode字符,那么它的内部实现是怎么样的呢?在Python中,这个事情比较复杂,在Python3.3之前,string其实默认是一个16-bit wchar_t
数组,不足以用来表示所有的Unicode码位,只能表示Basic Multilingual Plane (“BMP”)里面定义的Unicode子集。如果要让Python支持更多的Unicode码位该怎么办?只能重新编译Python,使用32-bit wchar_t
数组表示string,这样会致使很多内存浪费掉。
Python3.3实现了PEP 393 – Flexible String Representation,不再采用定长的数组来表示string,这样增加了灵活度,也节省了空间,但是增加了实现复杂度。
Go语言就比较简单了,看起来它的string内部直接采用UTF-8来表示。
小结
- 支持Unicode的编程语言通常采用不同的数据类型来表示文本和数据,文本通常用字符串(string)类型表示,数据通常用字节数组来表示。
- 从字符串到字节数组的转化过程中通常伴随着Unicode编解码的情况发生。
- 编程语言的源代码本身是以某种编码后的文本形式存在的,所以源代码中的字符串字面值(string literal)也是带有编码的,通常跟源代码的文本编码一致。
- 关于针对Unicode更复杂的话题,可参考:
(完)