这是一个很大的话题! 完全可以为它写一本厚书,事实上也有不少人这么做了(在 Amazon 上搜索“unicode book”就能看到一些例子)。
在更早的几十年里,计算机处理字符要简单得多,因为当时的程序员认为英语是唯一重要的语言。 于是,26 个字母(大写和小写)、10 个数字、几个标点符号,再加上一个用来响铃的编码(0x07),全部装进了 7 位里:这就是 ASCII 字符集。
很自然地,有人开始问:那 à、ä 和 Ł 呢?接着又有其他人问起 ऄ、ஹ 和 ญ,而年轻人则想要 emoji 😱。 该怎么办呢?
长话短说,许多聪明又有耐心的人在委员会里工作了好几年,敲定 Unicode 字符集以及 UTF-8 这类编码的细节,而大量软件也需要进行极其复杂的重写。 与此同时,还引入了许多新 bug。
为了防止所有东西都出问题,Unicode/UTF-8 的设计保证前 127 个编码与 ASCII 完全相同*(连响铃也不例外)*。
2005 年前后之后设计的语言有一个巨大的优势:一个相当稳定的 Unicode 标准已经存在。
Julia(2012 年首次发布)可以假定一切都将是 Unicode:字符、字符串、变量名和函数名、数学运算符……
引用手册中的话:“Julia 让处理纯 ASCII 文本变得简单而高效,处理 Unicode 也尽可能简单高效。”注意“尽可能”这几个字,它是这句话里很重要的一部分。
字符字面量用单引号书写,与用双引号书写的字符串不同。
这对来自 C/C++ 世界的人来说显而易见,但对 Python 和 JavaScript 程序员来说可能会让人困惑。
julia> a = 'a' # Roman alphabet
'a': ASCII/Unicode U+0061 (category Ll: Letter, lowercase)
julia> typeof(a)
Char
julia> jha = 'झ' # Devanagari alphabet
'झ': Unicode U+091D (category Lo: Letter, other)
julia> typeof(jha)
Char
julia> '❤' # heart emoji
'❤': Unicode U+2764 (category So: Symbol, other)
从这些例子可以看到,类型是 Char,而且 Julia 还提供了关于字符类别的更多信息。
仔细观察会发现,这些字符可以用 4 个十六进制数字表示。 完整的字符集最多需要 6 个十六进制数字。
这些数字称为“码点”,目前范围从 U+0000 到 U+10FFFF。
它们会在 REPL 中显示出来,但在代码里要用 codepoints() 获取。
在 Char 和 Int 之间转换很简单:
julia> Int('a')
97
julia> Char(97)
'a': ASCII/Unicode U+0061 (category Ll: Letter, lowercase)
编译器允许对 Char 进行某些形式的整数运算:
julia> 'b' - 'a' # interval, in alphabetic order
1
julia> 'b' + 'a'
ERROR: MethodError: no method matching +(::Char, ::Char)
julia> 'a' + 5
'f': ASCII/Unicode U+0066 (category Ll: Letter, lowercase)
julia> 'f' + ('A' - 'a') # same as `uppercase('f')`
'F': ASCII/Unicode U+0046 (category Lu: Letter, uppercase)
一部分字符串处理函数也能作用于 Char 输入。
uppercase() 和 lowercase() 改变大小写。isuppercase() 和 islowercase() 检测大小写。isletter(),覆盖多种字母表isdigit(),严格检测 0:9isnumeric(),比 isdigit 更宽泛,对 ¾ 和各种非欧洲文字返回 true
isxdigit(),十六进制数字isascii(),Unicode 之前的字符ispunct(),标点符号isspace(),任意空白字符isprint(),可打印字符(相反的是 iscntrl())islowercase('A') # false
uppercase('γ') # 'Γ': Unicode U+0393 (category Lu: Letter, uppercase)
ispunct('@') # true
isdigit('A') # false
isxdigit('A') # true
要检查某个字符是否出现在字符串中,可以用 in。
注意,这与子字符串不同:
# char search
julia> 'a' ∈ "xabcahliazlnkiw"
true
# substring search
julia> "a" ∈ "xabcahliazlnkiw"
use occursin(needle, haystack) for string containment
此外,正则表达式(属于另一个概念)能实现强大的搜索和操作。
要把字符串转换为字符向量,可以用 collect()。
要把字符向量转换为字符串,可以用 String() 构造函数。
julia> s = "abcde"
"abcde"
julia> cv = collect(s)
5-element Vector{Char}:
'a': ASCII/Unicode U+0061 (category Ll: Letter, lowercase)
'b': ASCII/Unicode U+0062 (category Ll: Letter, lowercase)
'c': ASCII/Unicode U+0063 (category Ll: Letter, lowercase)
'd': ASCII/Unicode U+0064 (category Ll: Letter, lowercase)
'e': ASCII/Unicode U+0065 (category Ll: Letter, lowercase)
julia> String(cv)
"abcde"
这对任何字符都适用,不只是 ASCII。
julia> collect("❤,😱")
3-element Vector{Char}:
'❤': Unicode U+2764 (category So: Symbol, other)
',': ASCII/Unicode U+002C (category Po: Punctuation, other)
'😱': Unicode U+1F631 (category So: Symbol, other)
注意,String() 构造函数作用于向量。
要把单个 Char 强制转换为长度为 1 的字符串,应使用 string() 函数,注意是小写的 s。
julia> string('a')
"a"
到目前为止,本文里的内容似乎都相对简单,那么真的没什么可担心的吗?
可惜,这种想法太乐观了!
一个麻烦来自每个码点“最多”需要 6 个十六进制数字。 这意味着,当用 UTF-8 编码时,不同的字符在内存中需要不同大小的空间。
一个字节只能存储最大到 255 的(无符号)数字,也就是两个十六进制数字,因此 UTF-8 用可变数量的字节(1 到 4 个)来存储一个 Char。
这些字节称为“代码单元”,ncodeunits() 函数会返回给定字符所需的代码单元数量。
julia> codepoint(jha) # jha 'झ' is defined in an earlier example
0x0000091d
julia> ncodeunits(jha)
3
julia> ncodeunits('a') # ASCII character
1
julia> ncodeunits('😱') # emoji
4
另外,并非所有能显示在屏幕上的东西都有自己独一无二的码点。 有些视觉上不同的字符被视为派生自其他字符,因此 Unicode 把它们当作一个父字符加上一个修饰符来处理。