在 Julia 中,String是有序的字符序列。正如手册所指出的:“当然,真正的麻烦在于追问一个字符到底是什么。”
其中很多复杂之处会在Chars概念中更详细地讲到,这里先简单说几点:
一般来说,字符串用双引号" "括起来。
单引号' '只用于Chars。
也可以使用三引号""" """,这样字符串里就能直接使用",不必转义。
更重要的是,多行字符串会自动去掉多余的缩进。
julia> """
Multiline
String
"""
"Multiline\nString\n"
和从 C 开始的许多语言一样,Julia 用反斜杠\来转义:
\n。\$(见下文)。字符串是可迭代的集合,所以for ... in循环可以直接用,无需先转换成字符向量。
julia> s = "Julia"
"Julia"
# admittedly, this is a silly example
julia> for c in s; print(c, "; "); end
J; u; l; i; a;
这样迭代的好处是,即使是 ASCII 之外的字符串,Julia 也能正确处理字符边界。 这一点的重要性在下一节会更清楚。
原则上,字符串完全可以像向量那样索引:
julia> s[1], s[5]
('J', 'a')
对 "Julia" 这样的 ASCII 字符串来说这当然没问题,但这样做忽略了世界上大多数语言。
来看看 Beowulf 中的一个字符,这部作品大约 15 个世纪前用古英语写成。
"Hrōðgār" 显然有 7 个字符,但并不是所有字符都属于 ASCII。
julia> king = "Hrōðgār"
"Hrōðgār"
julia> length(king)
7
julia> isascii(king)
false
# collect() converts to a Char vector:
julia> collect(king)
7-element Vector{Char}:
'H': ASCII/Unicode U+0048 (category Lu: Letter, uppercase)
'r': ASCII/Unicode U+0072 (category Ll: Letter, lowercase)
'ō': Unicode U+014D (category Ll: Letter, lowercase)
'ð': Unicode U+00F0 (category Ll: Letter, lowercase)
'g': ASCII/Unicode U+0067 (category Ll: Letter, lowercase)
'ā': Unicode U+0101 (category Ll: Letter, lowercase)
'r': ASCII/Unicode U+0072 (category Ll: Letter, lowercase)
像这样对字符串做索引,一开始很顺利,但到某个地方就会出问题:
julia> king[3]
'ō': Unicode U+014D (category Ll: Letter, lowercase)
julia> king[4]
ERROR: StringIndexError: invalid index [4], valid nearby indices [3]=>'ō', [5]=>'ð'
问题在于,这种索引方式是按_字节_计数的,而对非 ASCII 字符串来说,字节和字符之间并不是一一对应的关系。
一个 Unicode/UTF-8 字符最多可能需要 4 个字节来表示,Chars概念会进一步探讨这个问题。
很多语言用+作为字符串拼接运算符,但 Julia 不是这样。
至少它的报错信息还挺有用。
julia> "s1 " + "s2"
ERROR: MethodError: no method matching +(::String, ::String)
The function `+` exists, but no method is defined for this combination of argument types.
String concatenation is performed with *
julia> "s1 " * "s2"
"s1 s2"
string()函数可以拼接任意数量的字符串。
julia> string("lots ", "of ", "bits")
"lots of bits"
join()函数也能做到这一点,而且它还能接收一个字符串向量,用指定的分隔符把它们拼接起来(默认分隔符是"")。
julia> join(["lots", "of", "bits"], "_")
"lots_of_bits"
注意,字符串是不可变的,拼接会涉及复制。 因此,在循环里反复拼接效率很低,更好的做法是把各个片段收集到一个向量里,最后再一次性拼接起来。
很简单:
julia> repeat("Abc", 5)
"AbcAbcAbcAbcAbc"
很多语言都有把计算得到的值插入字符串的办法,不过对于用什么语法最好,大家的看法千差万别。
在 Julia 中,被插值的值前面要加$。
julia> myvar = 42
42
julia> "myvar evaluates to $myvar"
"myvar evaluates to 42"
julia> r = 5.3
5.3
julia> "A circle of radius $r has area $(π * r^2)"
"A circle of radius 5.3 has area 88.24733763933729"
如果省略括号会产生歧义,就必须加上括号;而对于后面紧跟空白的单个标识符,括号则是可选的。
要在字符串里包含 $,需要把它转义成\$。
注意这个限制:目前的插值不支持格式化,比如指定显示几位小数。
变通办法包括:
round()函数里。@sprintf()宏会在下面介绍。@sprintf()为了更精细地控制字符串的组装,Julia 从 C 借用了sprintf函数(后来的许多语言也有:维基百科列出了大约 30 种)。
在 Julia 中,它以macro的形式实现在Printf模块里,所以名称带有@前缀。
julia> using Printf
julia> r = 5.3
5.3
julia> @sprintf("A circle of radius %.1f has area %.2f", r, π * r^2)
"A circle of radius 5.3 has area 88.25"
为简单起见,本节只讲那些返回新字符串、不改变输入的函数。
其中许多都有一个在名字后面加!的等价版本,会就地修改输入。
一个字符串有多长? 有时候,这取决于你怎么数。
最简单的情况是 ASCII 字符串,每个字符占 1 个字节。
julia> string_asc = "Julia"
"Julia"
julia> length(string_asc)
5
julia> sizeof(string_asc)
5
length()函数返回_字符_数,sizeof()函数返回_字节_数。
换成其他字符集时,这个区别就很重要了:
julia> king = "Hrōðgār"
"Hrōðgār"
julia> length(king)
7
julia> sizeof(king)
10
编程中一个常见的操作,是把一个长字符串按一个或多个字符组成的分隔符拆成若干片段。
最通用的做法是用split()函数。
分隔符默认是(任意)空白字符,也可以自己指定。
julia> split("my little string")
3-element Vector{SubString{String}}:
"my"
"little"
"string"
julia> split("My_snake_case_string", "_")
4-element Vector{SubString{String}}:
"My"
"snake"
"case"
"string"
这些例子是把字符串拆成更小的_字符串_。 而把字符串拆成_字符_则是另一种操作。
julia> collect("input string")
12-element Vector{Char}:
'i': ASCII/Unicode U+0069 (category Ll: Letter, lowercase)
'n': ASCII/Unicode U+006E (category Ll: Letter, lowercase)
'p': ASCII/Unicode U+0070 (category Ll: Letter, lowercase)
'u': ASCII/Unicode U+0075 (category Ll: Letter, lowercase)
't': ASCII/Unicode U+0074 (category Ll: Letter, lowercase)
' ': ASCII/Unicode U+0020 (category Zs: Separator, space)
's': ASCII/Unicode U+0073 (category Ll: Letter, lowercase)
't': ASCII/Unicode U+0074 (category Ll: Letter, lowercase)
'r': ASCII/Unicode U+0072 (category Ll: Letter, lowercase)
'i': ASCII/Unicode U+0069 (category Ll: Letter, lowercase)
'n': ASCII/Unicode U+006E (category Ll: Letter, lowercase)
'g': ASCII/Unicode U+0067 (category Ll: Letter, lowercase)
字符串里是否包含某个子串?
奇怪的是,有两个函数可以做这件事:occursin()和contains(),它们的区别只在于实参的顺序。
julia> needle = "Julia"
"Julia"
julia> haystack = "Python, Julia and R"
"Python, Julia and R"
julia> occursin(needle, haystack)
true
julia> contains(haystack, needle)
true
如果要更具体地判断位置:
julia> startswith(haystack, "Python")
true
julia> endswith(haystack, "Python")
false
要得到子串的起始和结束下标,有这几个函数:
julia> findfirst(needle, haystack)
9:13
要替换子串,可以用x => y语法列出所有改动。
julia> replace("abc", "a"=>"b", "b"=>"c", "c"=>"a")
"bca"
从 Julia 1.7 起,replace函数可以接受任意数量的改动,并保证在替换过程中每个字符最多只会被改动一次。
x => y这种语法叫做pair,它是 Julia 中一种重要的类型,Dicts and Pairs概念里会有更详细的介绍。
注意:为简单起见,上面所有例子都用的是字符串字面量。 这些函数中有很多更通用,也可以用于正则表达式。 我们会在后面的概念中再回来讲这个。
我们经常需要去掉字符串开头或结尾的空白。
用于此的函数有lstrip()(左)、rstrip()(右)、strip()(两端)。
julia> strip("\n\t useful_bit\n\n")
"useful_bit"
这些函数的名字基本能说明它们的用途:
julia> s = repeat("aBcD ", 5)
"aBcD aBcD aBcD aBcD aBcD "
julia> uppercase(s)
"ABCD ABCD ABCD ABCD ABCD "
julia> lowercase(s)
"abcd abcd abcd abcd abcd "
julia> titlecase(s) # Initial letter of each word uppercase
"Abcd Abcd Abcd Abcd Abcd "
在数字和它的字符串表示之间来回转换往往很有用。
从数字转成字符串很简单,只不过要指定十进制以外的进制时需要多一个参数:
julia> string(42)
"42"
julia> string(42, base=16)
"2a"
julia> string(42, base=2)
"101010"
把字符串解析成数字稍微复杂一些,也更容易出错。 必须指定目标数字类型。 进制是可选的,默认为 10。
julia> parse(Int, "42")
42
julia> parse(Float64, "42")
42.0
julia> parse(Int, "42?")
ERROR: ArgumentError: invalid base 10 digit '?' in "42?"
julia> parse(Int, "42 ") # whitespace may be tolerated
42
有时候,我们需要在字符串中屏蔽掉通常的插值和转义。
为此,可以在开头的引号前加上raw。
julia> x = 10
10
julia> "x = $x if we use \$ for interpolation"
"x = 10 if we use \$ for interpolation"
julia> raw"x = $x if we use \$ for interpolation"
"x = \$x if we use \\\$ for interpolation"
正则表达式有自己复杂的语法,Julia 为此提供了两种很有用的字符串类型:r" "用于正则表达式,s" "用于处理捕获到的片段。
还有其他各种特殊字符串,在 Exercism 风格的编程中不太常见,但在编写库时用得很多。
v"x.y.z"用于处理软件发布中的主版本.次版本.修订号编号。b" "绕过 Unicode 的字符边界,直接在字节层面操作。