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()。反之,从bytesstr的转化需要通过解码过程,也就是要调用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的码位。此外,有两个相应的函数chrord来帮助进行码位和字符之间的转化:

>>> 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更复杂的话题,可参考:

(完)