这是一个_很大_的话题! 完全可以为它写一本厚厚的书,而且确实有好几个人这么做了(在亚马逊上搜索 “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
要把字符串转成 Char 向量,可以用 collect()。
要把 Char 向量转成字符串,可以用 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 _转换_成只有一个字符的字符串,用的函数是 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 把它们当作一个父字符加上一个修饰符。
这个问题会影响字符串,给下标操作带来挑战。
在本练习中,你将实现一组实用例程中的一部分,帮助开发者清理标识符名称。
在这 6 个任务中,你将逐步构建 transform(转换单个字符)和 clean(转换字符串)这两个函数。
合法的标识符由零个或多个字母、下划线、连字符、问号和 emoji 组成。
如果向 clean 函数传入空字符串,则应返回空字符串。
实现 transform 函数,将任何连字符替换为下划线。
julia> transform('-')
"_"
移除所有空白字符,包括开头和结尾的空白字符。
julia> transform(' ')
""
修改 transform 函数,将 camelCase 转换为 kebab-case。
julia> transform('D')
"-d"
修改 transform 函数,省略所有数字字符。
julia> transform('7')
""
修改 transform 函数,替换 'α' 到 'ω' 范围内的所有希腊字母。
julia> transform('β')
"?"
实现 clean 函数,将这些操作应用到一个完整的字符串上。
不符合上述规则的字符应原样保留。
julia> clean(" a2b Cd-ω😀 ")
"ab-cd_?😀"