Julia編程基礎(六):玩轉字符和字符串

本文是《Julia 編程基礎》開源版本第六章:字符和字符串。本書旨在幫助編程愛好者和專業程序員快速地熟悉 Julia 編程語言,並能夠在夯實基礎的前提下寫出優雅、高效的程序。這一系列文章由 郝林 採用 CC BY-NC-ND 4.0知識共享 署名-非商業性使用-禁止演繹 4.0 國際 許可協議)進行許可,請在轉載之前仔細閱讀上述許可協議。

6.1 Unicode 字符

在講 Unicode 字符之前,我們先來簡要介紹一下 ASCII 編碼。

ASCII 是 American Standard Code for Information Interchange 的縮寫,可以翻譯爲美國信息交換標準代碼。它是由美國國家標準學會(American National Standard Institute, 簡稱 ANSI)制定的標準的單字節字符編碼方案,主要用途是基於文本的數據交換。這個方案起始於上個世紀 50 年代的後期,並在 1967 年定案。它最初是美國的國家標準,是不同計算機在相互通信時共同遵守的西文字符編碼標準。ASCII 編碼支持的所有字符的集合被稱爲 ASCII 編碼集。

ASCII 編碼使用 1 個字節來表示 1 個字符。其中的 7 位二進制數字用於表示大寫和小寫的英文字母、09的數字、各種英文標點符號,以及一些不可打印字符和控制字符。而字節的最高位則用於奇偶校驗。這使得 ASCII 編碼集中只能容納 128 個字符。

我們之前提到的 Unicode 字符實際上指的是 Unicode 編碼標準所支持的字符。Unicode 是一個針對書面字符和文本的通用字符編碼標準。它定義了多語言文本數據在國際間交換的統一方式,併爲全球化軟件創建了基礎。Unicode 編碼標準以 ASCII 編碼集作爲出發點,並突破了 ASCII 只能對拉丁字母進行編碼的限制。它提供了可以對世界上的所有語言中的所有文字進行編碼的能力,其支持的字符已超過百萬。此外,它還支持所有已知的轉義序列和控制代碼。

在計算機系統內部,抽象的字符被編碼爲數字。用於代表抽象字符的整數範圍被稱爲代碼空間(code space)。代碼空間中的每一個特定整數都被稱爲代碼點(code point)。當一個抽象字符被映射到(或者說被分配給)一個代碼點時,這個代碼點就可以被看成一個已編碼的字符。

在 Unicode 編碼標準中,代碼空間由從010FFFF的十六進制整數組成。這就意味着,有 1114112 個代碼點可以用於表示抽象字符。Unicode 編碼標準的慣用法是使用十六進制形式來表示代碼點的數值,並使用U+作爲前綴。比如,英文字母字符'a'的 Unicode 代碼點就是U+0061。並且,一個受到支持的字符能且僅能由一個對應的 Unicode 代碼點表示。

我們已經知道,在計算機系統中,整數可以由固定大小的代碼單元(code unit)來表示。比如,8 個比特(也就是 1 個字節)、16 個比特或 32 個比特的代碼單元,等等。在 Unicode 編碼標準的模型中,編碼格式用於確定怎樣將代碼空間中的每一個整數(或者說代碼點)都表示成包含若干個代碼單元的序列。Unicode 編碼標準中存在多種編碼格式。其中有一種編碼格式叫做 UTF-8。UTF 是 Unicode Transformation Format 的縮寫。

UTF-8 編碼格式以單個字節爲 1 個代碼單元,並且完全兼容 ASCII 編碼。換句話說,對於這種編碼格式,Unicode 代碼點U+000U+007F的編碼即爲0x000x7f,並且它們所代表的含義與 ASCII 編碼中的完全一致。另外,UTF-8 是一種寬度可變的編碼格式。它會根據字符的不同,用 1 至 4 個代碼單元來編碼一個字符。比如,對於中文、日文和韓文中的一個字符,它會使用 3 個代碼單元來表示。也就是說,對於這些 Unicode 字符,UTF-8 會把它們轉換成寬度爲 3 個字節的二進制數。至於它是怎樣轉換的,我們就不在此討論了。

關於 Unicode 編碼標準和 UTF-8 編碼格式的更多知識,你可以參看 Unicode 官方網站中提供的文檔。

總之,Unicode 編碼標準幫助我們屏蔽掉了各種字符的複雜性,並且已被普遍認爲是解決這一問題的終極標準。而 UTF-8 則是 Julia 所使用的編碼格式。Julia 通過此編碼格式支持所有的 Unicode 字符。

對於字符和字符串,Julia 通常都會採用 UTF-8 編碼格式將它們轉換成二進制數並進行存儲。

6.2 字符

從表面上看,每一個字符都是一個獨立且不可再分割的圖形。但不要忘記,從存儲的層面看,我們還可以把它們拆分成一個個代碼單元,甚至一個個比特。

6.2.1 值的表示與操作

Julia 中的一個字符值只能容納一個 Unicode 字符。並且,每個字符值都需要由一對單引號包裹。通過 REPL 環境,我們可以很方便地獲知任何字符值的細節,例如:

julia> 'a'
'a': ASCII/Unicode U+0061 (category Ll: Letter, lowercase)

julia> '中'
'中': Unicode U+4e2d (category Lo: Letter, other)

julia> 

其中,對於我們來說比較重要的是回顯內容中的 Unicode 代碼點。比如,字符'a'的 Unicode 代碼點是U+0061。而ASCII/Unicode的意思是,'a'同時也是 ASCII 編碼所支持的字符,而且 ASCII 和 UTF-8 對它編碼之後產生的整數是相同的。至於括號中的代碼點分類等信息,我們一般不用太關注。

除了在一對單引號之間直接插入一個 Unicode 字符,我們還可以用另外兩種標準的方式來表示一個字符值。一種方式是,以\u爲前綴並後跟代表了某個 Unicode 代碼點的十六進制數,最多 4 個數字。另一種方式與之類似,以\U爲前綴並後跟代表了某個 Unicode 代碼點的十六進制數,最多 8 個數字。注意,對於後一種方式,實際上後跟 6 個十六進制數字就足夠了。如果最左邊的 2 個十六進制數字不是0,那麼它肯定就超出了 Unicode 的代碼空間。下面是一些示例:

julia> '\u4e2d'
'中': Unicode U+4e2d (category Lo: Letter, other)

julia> '\U004e2d'
'中': Unicode U+4e2d (category Lo: Letter, other)

julia> '\U10ffff'
'\U10ffff': Unicode U+10ffff (category Cn: Other, not assigned)

julia> '\U0110ffff'
ERROR: syntax: invalid escape sequence

julia> 

此外,我們還可以用'\xHH''\OOO'的形式表示一個可由 ASCII 編碼的字符值。其中,HH的意思是至多 2 個十六進制的數字,而OOO的含義是至多 3 個八進制的數字。例如:

julia> '\x61'
'a': ASCII/Unicode U+0061 (category Ll: Letter, lowercase)

julia> '\141'
'a': ASCII/Unicode U+0061 (category Ll: Letter, lowercase)

julia> 

還有,那些經典的轉義序列(escape sequence)也可以被用在這裏。比如,'\t'表示製表符,'\n'表示換行符,等等。

在某些情況下,由於一些原因(如字面量衝突、含義衝突等)我們無法在代碼中直接寫出需要使用的字符。這時就要用到轉義,也就是用多個字符的有序組合來代表原本需要的字符。這裏的多個字符的有序組合就叫做轉義序列。

那什麼叫做經典的轉義序列呢?它是指,針對於 ASCII 編碼集中的那些不可打印字符的轉義序列。這些轉義序列最早是在 C 語言中被定義的,後來又被很多編程語言沿用。它們的字面量與原字符的編碼值是無關的,但或多或少會與其含義存在一些關聯。詳見下表。

表 6-1 經典的轉義序列

轉義序列 ASCII 編碼值 含義
\0 0 空字符(null)
\a 7 響鈴(bell)
\b 8 退格(backspace)
\f C 換頁(new page)
\n A 換行(new line)
\r D 回車(carriage return)
\t 9 水平製表(horizontal tab)
\v B 垂直製表(vertical tab)

提示一下,該表中表示 ASCII 編碼值的那些整數都是十六進制的。

由於轉義序列都是以\爲前綴的,所以當我們想表示一個反斜槓的時候就需要在它的前面再加一個反斜槓,以說明後面的反斜槓代表的並不是轉義序列的前綴,如:

julia> '\\'
'\\': ASCII/Unicode U+005c (category Po: Punctuation, other)

julia> 

回顯內容中的005c就是反斜槓的 ASCII 編碼值。

另外,由於字符值都需要以單引號包裹,所以如果我們想表示單引號本身的話,那麼也要用反斜槓轉義一下:

julia> '\''
'\'': ASCII/Unicode U+0027 (category Po: Punctuation, other)

julia> 

回顯內容中的0027就是單引號的 ASCII 編碼值。

最後,順便提一下,比較操作符是支持字符值的。這種比較也是基於 Unicode 代碼點的。比如:

julia> 'A' < 'a' < '中'
true

julia> 

另外,加法和減法也可以作用於字符值。例如,運算表達式'A' + 32的求值結果就是'a'。這表明字符'A''a'的 Unicode 代碼點相差32

6.2.1 類型與轉換

在 Julia 中,字符值的默認類型是CharChar類型是一個寬度爲 32 個比特的原語類型。顯然,這個類型的值足夠裝下任何一個採用 UTF-8 編碼的 Unicode 代碼點。同時,它也是抽象類型AbstractChar的子類型,還是 Julia 預定義的唯一的一個具體的字符類型。

從存儲層面看,Char類型與UInt32類型幾乎是相同的。因此我們可以說,字符值就相當於無符號的整數。我們可以很輕易地把一個字符值轉換成一個整數值,反之亦然。示例如下:

julia> UInt32('中')
0x00004e2d

julia> Char(0x00004e2d)
'中': Unicode U+4e2d (category Lo: Letter, other) 

julia> Int64('中')
20013

julia> Char(20013)
'中': Unicode U+4e2d (category Lo: Letter, other)

julia> 

但要注意,並不是所有的UInt32類型的值都會代表一個有效的 Unicode 代碼點。比如:

julia> Char(0x11ffff)
'\U11ffff': Unicode U+11ffff (category In: Invalid, too high)

julia> 

回顯的括號中已有明確的提示,整數值0x11ffff是一個無效的 Unicode 代碼點。因爲它太高了,超出了 Unicode 編碼標準所定義的代碼空間。對於這種有效性的判斷,我們可以使用isvalid函數:

julia> isvalid('中')
true

julia> isvalid(Char(0x00004e2d))
true

julia> isvalid(Char(0x11ffff))
false

julia> 

此外,我們總是可以用codepoint函數把一個字符值轉換成一個整數值:

julia> codepoint('中')
0x00004e2d

julia> typeof(ans)
UInt32

julia> 

注意,對於用不同格式編碼的字符值,codepoint函數的結果值的類型可能會不同。但可以確定的是,它總會返回一個整數值。

6.3 字符串

雖然字符串通常會由一個個字符組成,但在 Julia 中,字符串與字符卻是截然不同的兩個概念。

6.3.1 值的表示

一個字符串值一般由一對雙引號包裹,並可以包含零到多個字符:

julia> ""
""

julia> "a"
"a"

julia> "Julia"
"Julia"

julia> 

我們也可以用三聯雙引號來包裹這類值。在這種情況下,我們輸入的字符串可以跨越多個行。其中的換行都會以換行符的形式保留下來,但緊跟在第一個三聯雙引號後面的換行會被忽略。對於回車以及回車換行的組合也是如此。下面是一個示例:

julia> """
       \u263c CN        
       US
       EN
       R\125 \n\t
       """
"☼ CN        \nUS\nEN\nRU \n\t\n"

julia> 

注意,我在CN的右邊用 Tab 鍵輸入了一個製表符,所以在回顯內容的對應位置上存在一段空白。回顯內容中最左邊的是一個由 Unicode 代碼點\u263c代表的字符。而在空白的右邊,針對我輸入的每一個換行都存在一個換行符\n。另外,\125也是一個轉義序列。其中的125是一個八進制的 ASCII 編碼值。這個轉義序列對應於大寫字母U

你可能已經看到,我直接寫入的轉義序列\n\t都被原封不動地保留了下來。這裏的規則是,字符串值總是會原樣保留那些經典的轉義序列。對於我們之前提到的針對反斜槓的轉義序列\\也是如此。至於其他的轉義序列,它們仍然會像以前那樣被處理。

另外,在前面的多行字符串中,一些用於縮進的空白(包括空格和製表符)並沒有被識別爲字符串的一部分。這又是爲什麼呢?實際上,對於由三聯雙引號包裹的字符串值,Julia 會以縮進最少的那一行爲基準來保留每一行中的前置空白。注意,第一行以及只包含空格和製表符的行並不會被作爲基準。示例如下:

julia> """
       Julia
       Python
       Golang
       Java
       """
"Julia\nPython\nGolang\nJava\n"

julia> 

在這個多行字符串中,從Julia到第二個三聯雙引號的 5 行裏,它們的縮進都是一樣的。所以,回顯的字符串值中不存在任何的空格。但如果我們調整一下,相應的空格就會出現:

julia> """
           Julia
             Python
               Golang
                 Java
         """
"  Julia\n    Python\n      Golang\n        Java\n"

julia> 

我們依然來看從Julia到第二個三聯雙引號的 5 行。其中,最後一行的縮進是最少的,只有 9 個空格。所以,對於其他行的前置空格,都要被剪掉 9 個。而剩下的空格都會被原樣地保留在字符串值中。下面是另一個例子:

julia> """
       Julia
         Python
           Golang
             
             Java
         """
"Julia\n  Python\n    Golang\n      \n      Java\n  "

julia> 

顯然,對於這個多行字符串,Julia 在考慮前置空白的保留問題時,是以Julia那一行爲基準的。

最後,對於由雙引號包裹的字符串值,如果我們想在其中表示雙引號本身,那麼就要用反斜槓進行轉義。比如,字符串值"\""的實際內容是"。但在由三聯雙引號包裹的值中,表示雙引號卻用不着轉義。

6.3.2 類型之上的設定

字符串值的默認類型是StringString是抽象類型AbstractString的子類型之一。Julia 對字符串的很多設定都是基於這個抽象類型展開的。

首先,一個字符串就是一個包含了若干個代碼單元的序列。還記得嗎?我們說過 UTF-8 的代碼單元是 1 個字節。這可以通過調用codeunit函數來驗證:

julia> comment1 = "codeunit 函數會返回給定字符串對象的代碼單元類型"
"codeunit 函數會返回給定字符串對象的代碼單元類型"

julia> codeunit(comment1)
UInt8

julia> 

這個函數可以接受一個AbstractString類型的參數值,並返回它的代碼單元的類型。上述字符串的代碼單元類型是UInt8,即寬度爲 1 個字節的無符號整數類型。

其次,既然字符串是代碼單元的序列,那麼就應該可以抽取出其中的代碼單元。事實也確實如此。這仍然需要用到codeunit函數。我們可以把一個字符串值和某個有效的索引號同時傳給它,比如:

julia> codeunit(comment1, 1)
0x63

julia> typeof(ans)
UInt8

julia> 

調用表達式codeunit(comment1, 1)的含義是,從comment1代表的字符串值中抽取出第 1 個代碼單元。這時,codeunit函數會返回一個UInt8類型的值,即:那個與給定索引號對應的代碼單元。此外,還有一個名稱與之很像的函數codeunits。它可以返回一個由字符串值中的所有代碼單元組成的序列。

我們都知道,字符串在底層都是由一個個字節組成的。而所謂的索引號,就是指字符串中的字節的序號。對於採用 UTF-8 編碼的字符串來說,字節的序號就等於代碼單元的序號。

那什麼叫做有效的索引號呢?對於一個字符串值來說,有效的索引號是從1開始的。1就是有效索引號的下限。這與很多其他的編程語言中的設定都不同。更寬泛地講,Julia 中的索引號一般都必須是正整數,而不能是0

那麼,字符串值中的有效索引號的上限又是多少呢?在這裏,我們可以通過調用ncodeunits函數獲取到它。例如:

julia> ncodeunits(comment1)
66

julia> 

因此,字符串comment1的有效索引號的範圍就是[1, 66]。一旦索引號低於相應的下限或高於相應的上限,就會立即引發一個錯誤:

julia> codeunit(comment1, 0)
ERROR: BoundsError: attempt to access "codeunit 函數會返回給定字符串對象的代碼單元類型"
  at index [0]
Stacktrace:
 [1] checkbounds at ./strings/basic.jl:193 [inlined]
 [2] codeunit(::String, ::Int64) at ./strings/string.jl:89
 [3] top-level scope at none:0

julia> codeunit(comment1, 67)
ERROR: BoundsError: attempt to access "codeunit 函數會返回給定字符串對象的代碼單元類型"
  at index [67]
Stacktrace:
 [1] checkbounds at ./strings/basic.jl:193 [inlined]
 [2] codeunit(::String, ::Int64) at ./strings/string.jl:89
 [3] top-level scope at none:0

julia>

這就是第三個設定,即:對於一個字符串值,它的有效索引號一定大於或等於1,且小於或等於其中字節的個數。

6.3.3 操作字符串

基於 Julia 對通用字符串的三個基本設定,我們可以用很多方式來操作字符串。

6.3.3.1 獲取長度

關於獲取一個字符串值的長度,我們已經知道ncodeunits函數是可用的。這個函數會獲取字符串值中的代碼單元的數量(代碼單元長度)。這對於採用 UTF-8 編碼的字符串來說,就相當於獲取其中字節的數量。但如果爲了保險起見,我們可以用sizeof函數來獲取其中的字節個數(字節長度):

julia> sizeof(comment1)
66

julia> sizeof("a")
1

julia> sizeof("中")
3

julia> 

另外,若想得到一個字符串值中的字符的數量(字符長度),我們可以使用length函數。例如:

julia> length(comment1)
28

julia> length("a")
1

julia> length("中")
1

julia> 

這個函數還可以再接受兩個代表索引號的參數。此時它計算的就是某個字符串片段中的字符的個數。示例如下:

julia> comment1[1:13]
"codeunit 函數"

julia> length(comment1, 1, 13)
11

julia> length(comment1, 1, 12)
10

julia> length(comment1, 1, 10)
10

julia> 

可以看到,length函數並沒有把字符串片段中的不完整字符(或者說無效字符)計算在內。所謂的無效字符是指,根據既定的編碼格式(如 UTF-8),無法被識別和轉換爲一個字符的若干連續字節。它們可能只是某個字符編碼的一部分,也可能根本就無關於任何字符編碼。

不過,如果一個字符串(片段)中只包含了無效字符的話,那麼length函數還是會把它算作一個字符的:

julia> comment1[10:13]
"函數"

julia> length(comment1, 10, 13)
2

julia> length(comment1, 11, 13)
1

julia> 

索引號1112分別對應的是字符的後兩個代碼單元,而索引號13對應的則是字符的第一個代碼單元。這 3 個代碼單元合在一起並不能形成一個有效的 Unicode 代碼點。但由於這個字符串片段中只包含了這 3 個字節,所以length函數認爲其字符長度爲1,而不是0

這有時可能會讓我們感到困惑。比如:

julia> ich1, ich2, ch =  "\xe2\x88", "\x80", "\xe2\x88\x80"
("\xe2\x88", "\x80", "∀")

julia> length(ich1) + length(ich2) == length(ch)
false

julia> length(ich1), length(ich2), length(ch)
(1, 1, 1)

julia> 

如果我們把ich1ich2拼接在一起的話,就肯定會得到ch代表的字符串值。從直覺上講,它們的字符長度之間應該存在“和”的關係。但事實並非如此。如果遇到這樣的情況,我們可以用isvalid函數對這些字符串做一下有效性的判斷。再結合length函數的行爲特點,這通常就可以爲我們解惑了。

6.3.3.2 索引

我們可以用索引表達式從一個字符串值中抽取某個代碼單元。例如:

julia> comment1[1]
'c': ASCII/Unicode U+0063 (category Ll: Letter, lowercase)

julia> 

comment1[1]就是一個索引表達式。索引表達式通常由一個可索引對象以及一個由中括號包裹的索引號組成。這裏的可索引對象和索引號都不僅限於字面量,還可以是標識符或表達式,只要最終能代表它們就可以了。顯然,字符串值就是一種可索引對象。而這裏的索引號的有效範圍則依從於前面所述的基本設定。

66是一個有效的索引號。然而,當我們用它對comment1進行索引時,仍然會引發一個錯誤:

julia> comment1[66]
ERROR: StringIndexError("codeunit 函數會返回給定字符串對象的代碼單元類型", 66)
Stacktrace:
 [1] string_index_err(::String, ::Int64) at ./strings/string.jl:12
 [2] getindex_continued(::String, ::Int64, ::UInt32) at ./strings/string.jl:218
 [3] getindex(::String, ::Int64) at ./strings/string.jl:211
 [4] top-level scope at none:0

julia> 

這是爲什麼呢?其原因是,只有在索引到某個字符的第一個代碼單元時,索引表達式才能正確地獲取這個字符。因爲索引表達式的求值結果會是一個Char類型的字符值,而只拿到一個 Unicode 代碼點的某個部分是毫無意義的。這與codeunit函數的行爲截然不同。

還記得嗎?對於 UTF-8 編碼格式來說,一箇中文字符需要用掉 3 個代碼單元。在變量comment1代表的字符串中,最後一個字符是。因此,索引號66對應的應該是,表示該字符的那 3 個代碼單元中的最後一個。讓我們來驗證一下:

julia> comment1[66-2]
'型': Unicode U+578b (category Lo: Letter, other)

julia> 

果然,索引號64對應的就是的第一個代碼單元。這樣的索引號也可以被稱爲有效字符的起始索引號,簡稱字符索引號。

可是,對於一個外來的字符串值,我們怎麼知道其中的哪些索引號是字符索引號呢?難道需要逐個試錯嗎?幸好不用這樣。我們可以使用isvalid函數來做這樣的判斷:

julia> isvalid(comment1, 66), isvalid(comment1, 65), isvalid(comment1, 64)
(false, false, true)

julia> 

另外,還有一些函數可以幫助我們更好地索引字符串值中的字符。比如,firstindex函數會返回字符串值中的第一個字符索引號,通常就是1lastindex函數會返回字符串值中的最後一個字符索引號。對於comment1來說,它就是64。我們還可以使用關鍵字end來指代最後一個字符索引號。它可以被直接應用於索引表達式中。例如:

julia> comment1[end]
'型': Unicode U+578b (category Lo: Letter, other)

julia> comment1[end-3]
'類': Unicode U+7c7b (category Lo: Letter, other)

julia> 

當需要更加精確的索引時,我們可以使用thisind函數。示例如下:

julia> thisind(comment1, 10)
10

julia> thisind(comment1, 12)
10

julia> thisind(comment1, 13)
13

julia> 

這個函數接受兩個參數。第一個參數代表被索引的字符串值(以下簡稱slogan1),第二個參數代表索引號(以下簡稱ind)。如果ind對於slogan1來說正好是某個字符索引號(也就是說它對應於某個字符的第一個代碼單元),那麼該函數就會直接將ind的值返回。如果ind屬於其他的有效索引號,那麼與它對應的代碼單元肯定是某個字符(可稱當前字符)的後續部分之一。在這種情況下,thisind函數會向前尋找到當前字符的起始索引號,並將其返回。在上例中,調用表達式thisind(comment1, 12)就屬於這種情況。但無論怎樣,這個函數的結果值的類型一定會是Int64

擁有類似功能的函數還有previndnextindprevind函數可以返回在當前字符之前的第n個字符的起始索引號,而nextind函數則可以返回在當前字符之後的第n個字符的起始索引號。這裏的n默認是1,並可以由函數的調用方給定。

特別提醒一下,我們雖然可以通過索引表達式訪問到字符串值中的某個字符,但卻不可以修改其中的任何字符。其根本原因是,Julia 中的字符串值都是不可修改的!

6.3.3.3 截取

我們可以通過兩個索引號截取出一個字符串的某個片段:

julia> comment1[1:8]
"codeunit"

julia> 

不過要注意,只要有一個索引號不是字符索引號,這個索引表達式就會立即引發錯誤。示例如下:

julia> comment1[1:10]
"codeunit 函"

julia> comment1[1:11]
ERROR: StringIndexError("codeunit 函數會返回給定字符串對象的代碼單元類型", 11)
Stacktrace:
 [1] string_index_err(::String, ::Int64) at ./strings/string.jl:12
 [2] getindex(::String, ::UnitRange{Int64}) at ./strings/string.jl:248
 [3] top-level scope at none:0

julia> comment1[1:13]
"codeunit 函數"

julia> 

這種(範圍)索引表達式的求值結果會是一個String類型的字符串值。即使結果中只包含一個字符也是如此。這與普通的(點)索引表達式有着明顯的區別。

另外,範圍索引表達式的結果值其實是一個複本,拷貝自源字符串中的某個片段。如果被拷貝的字符過多,那麼有可能會對程序的性能產生一定的影響。爲了應對這種情況,我們可以基於某個字符串片段創建一個子字符串,以避免其中字符的拷貝。具體的做法是,使用SubString類型:

julia> func_name1 = SubString(comment1, 1, 8)
"codeunit"

julia> typeof(ans)
SubString{String}

julia> 

SubString類型的構造函數可以接受三個參數,第一個參數代表源字符串,後兩個參數都代表索引號。該函數會生成一個視圖,並基於此視圖創建一個子字符串的值。你可以把這裏的視圖理解爲一個窗口。這個窗口只能看到兩個給定索引號之間的那些字符。

子字符串的值與字符串值看起來沒有什麼兩樣。而且,針對後者的操作也基本上都能應用於前者:

julia> func_name1[1]
'c': ASCII/Unicode U+0063 (category Ll: Letter, lowercase)

julia> codeunit(func_name1, 1)
0x63

julia> ncodeunits(func_name1)
8

julia> func_name1[1:4]
"code"

julia> 

但是,SubString類型的實例創建成本往往明顯低於String類型。所以,我們在截取字符串片段的時候應該優先使用它。

6.3.3.4 拼接

我們有時候需要把多個字符串拼接在一起。這時就可以使用string函數:

julia> string("\xe2\x88", "\x80", "\xe2\x88\x80")
"∀∀"

julia> 

此外,操作符*也可以派上用場:

julia> "\xe2\x88" * "\x80" * "\xe2\x88\x80"
"∀∀"

julia> 

對於字符串值來說,*的含義就不再是“乘以”了,而是“拼接”。這個操作符被用在這裏可能會讓你感到有些不適應。因爲很多其他的編程語言都是用操作符+來拼接字符串的。

Julia 語言的締造者們是站在抽象代數的角度來看待這一問題的。在抽象代數中,+通常被用在那些滿足交換律的運算中,而*常常被用在不滿足交換律的運算中。對於字符串拼接來說,"A"拼接"B""B"拼接"A"肯定不是一回事,一定會得到不同的結果。所以,操作符*理應被用在這裏。

倘若你不熟悉抽象代數也沒有關係。你可以這樣來理解:數值相加是基於數學邏輯的運算,而字符串值的拼接則是基於空間的合併。所以它們理應使用不同的操作符號來表達。

無論怎樣,我們都應該記住:在 Julia 中,字符串拼接用的是操作符*,而不是+。並且,字符串拼接總會產生全新的字符串值。

6.3.3.5 插值

所謂的插值就是,在一個字符串值中動態地插入其他值。這需要把符號$作爲前綴。正因爲$在字符串中的作用特殊,所以纔有了轉義序列\$,以表示$字符本身。

還記得嗎?我們其實在第 1 章就用過插值。那時的代碼是這樣的:

println("Hey, $(name)!")

在這個字符串值中,$(name)就是那個動態的部分,也被稱爲插值部分。其含義是動態插入由標識符name代表的值。這部分可以被簡寫爲$name。不過,爲了保證不引起歧義,我用圓括號把這個標識符包裹了起來,以明確區別於其他的靜態字符。示例如下:

julia> name = "Robert"
"Robert"

julia> println("Hey, $(name)!")
Hey, Robert!

julia> name = "Eric"
"Eric"

julia> println("Hey, $(name)!")
Hey, Eric!

julia> name = "everyone"
"everyone"

julia> println("Hey, $(name)!")
Hey, everyone!

julia> 

可以看到,隨着我爲變量name綁定不同的值,println函數打印出的內容也在動態的改變。

其實,跟隨$的並不僅限於標識符,還可以是任何的表達式。例如:

julia> dup_chars = "\xe2\x88" * "\x80" * "\xe2\x88\x80"
"∀∀"

julia> "Is string $(dup_chars) valid? $(isvalid(dup_chars) ? "Yes" : "No")"
"Is the string ∀∀ valid? Yes"

julia> 

解釋一下,這裏的?是一個三元操作符。因此,表達式isvalid(dup_chars) ? "Yes" : "No"的含義就是,如果dup_chars代表的字符串值只包含有效字符,那麼就使用"Yes"作爲結果值,否則就使用"No"作爲結果值。

也許你已經發現了,在插值部分中,那些用於包裹字符串的雙引號(比如"Yes"中的雙引號)並不需要被轉義。這主要是因爲,插值部分相當於鑲嵌在字符串中的代碼,代碼中的雙引號自然用不着再轉義。但這有兩個前提條件,一個是插值部分必須有圓括號包裹,即:形如$(...)。另一個條件是其中的雙引號必須成對的出現。

另外,插值部分中的求值結果不僅可以是字符串值(如前面的"Yes""No"),還可以是其他任何類型的值。實際上,它們的值總會由string函數(還會涉及到print函數和show函數)轉換爲字符串值,或者說對象的規範文本表示形式。這種表示形式通常會以最簡單的文本展示出對象的內部狀態,並儘量避免暴露過多的細節。下面是一些例子:

julia> float1 = 2.1e-3; "value: $(float1)"
"value: 0.0021"

julia> complex1 = 0.1+0.02im; "value: $(complex1)"
"value: 0.1 + 0.02im"

julia> rational1 = 3//7; "value: $(rational1)"
"value: 3//7"

julia> "value: $('\u4e2d')"
"value: 中"

julia> "value: $(isvalid(Char(0x4e2d)))"
"value: true"

julia> "value: $(SubString("codeunit 函數", 1, 8))"
"value: codeunit"

julia> array1 = [2020, 2030, 2050]; "value: $(array1)"
"value: [2020, 2030, 2050]"

julia> 

6.3.3.6 搜索

我們可以利用一些函數在一個字符串值中搜索指定的字符串。我們可稱前者爲被搜索的字符串值,並稱後者爲目標字符串。

比如,函數findfirstfindlast,它們分別會以從前向後和從後向前的順序去搜索目標字符串,並會在碰到第一個匹配的字符串時停下來,然後返回與之對應的索引號範圍值,形如1:10。在此類值中,冒號左邊的正整數代表,目標字符串在被搜索的字符串值中的起始字符索引號。而冒號右邊的正整數則代表,目標字符串在被搜索的字符串值中的末尾字符索引號。下面我們來看一個例子:

julia> slogan1 = "Julia 編程入門很簡單。"
"Julia 編程入門很簡單。"

julia> findfirst("入門", slogan1)
13:16

julia> 

在這個調用表達式中,第二個參數值就是將要被搜索的字符串值,而第一個參數值則是我們給予的目標字符串。對於slogan1代表的值來說,目標字符串的起始字符在其中的字符索引號是13。相應的,它的末尾字符在其中的字符索引號是16。所以,這裏調用的結果就是13:16

類似的,函數findprevfindnext都會從給定的索引號開始搜索目標字符串。不同的是,前者會向前搜索,而後者會向後搜索。示例如下:

julia> slogan2 = "Julia 編程入門,跟着入門很簡單。"
"Julia 編程入門,跟着入門很簡單。"

julia> findprev("入門", slogan2, 19)
13:16

julia> findnext("入門", slogan2, 19)
28:31

julia> 

注意,在slogan2中存在兩個目標字符串。而且,我們給予這兩個函數的第三個參數值都是19,代表着中文逗號slogan2中的字符索引號。顯然,在參數值完全相同的情況下,findprev函數和findnext函數返回的結果值是不同的。

從 Julia 的 1.3 版本開始,上述 4 個函數還可以直接用於搜索指定的單個字符。它們會在找到字符後返回與之對應的字符索引號。不過,在這之前,我們也可以做到這一點,但需要多敲一些代碼,傳入一個用來做條件判斷的函數。比如:

julia> findfirst(isequal('門'),  slogan1) 
16

julia> 

注意,isequal('門')原本是一個調用表達式,但在這裏它代表的卻是一個匿名的函數。它表示的條件是“字符必須等於'門'”。

如果沒有找到目標字符或字符串,那麼這些函數就會返回nothing。這個nothing比較特殊,它是Nothing類型的唯一實例,用於表示一個表達式沒有實質的結果值,或者一個變量或字段沒有值。注意,nothing作爲求值結果在 REPL 環境中是不顯示的:

julia> findfirst(isequal('窗'),  slogan1) 

julia> findfirst(isequal('窗'),  slogan1) == nothing
true

julia> 

最後提一下,如果我們只想知道一個字符串值中是否存在某個字符或字符串,那麼就可以使用occursin函數。這個函數總是會返回一個Bool類型的結果值。比如,調用表達式occursin('窗', slogan1)的求值結果是false

6.3.3.7 比較

比較操作符也可應用於字符串值。我們在上一章說過,對於這類值,比較操作符會逐個字符地進行比較,並且忽略其底層編碼。對於默認的字符串值,以及任何符合 Unicode 編碼標準的字符串值(不論它們採用的是哪一個編碼格式),比較操作符都會基於 Unicode 代碼點對它們進行比較。如果字符串中只包含英文字母,那麼你也可以認爲它基於的是其中每個字符的字典順序。例如:

julia> "Julia" < "Julie"
true

julia> "Julia" < "Julian" 
true

julia> 

字符串"Julia""Julie"的前 4 個字母都是相同的。但是,前者的最後一個字母a在字典中比後者的最後一個字母e更靠前。所以,前者小於後者。

對於字符串"Julia""Julian",兩者的前 5 個字母都相同,且前者算是後者的一個前綴。在這種情況下,後者肯定大於前者。再來看一個例子:

julia> "Michael" < "Mike"
true

julia> 

雖然"Michael""Mike"更長,但是它的第 3 個字母c在字典中比"Mike"的第 3 個字母k更靠前,所以它是小於"Mike"的。

不過要注意,大寫的英文字母總是會小於小寫的英文字母。因爲前者的 Unicode 代碼點肯定比後者的 Unicode 代碼點要小。這與它們在 ASCII 編碼集中的順序也是相同的。比如,表達式"JuliA" < "Julia"的求值結果一定是true

我們再看中英文混合的情況:

julia> "Julia 編程入門" < "Julia 編程基礎"
true

julia> '入'
'入': Unicode U+5165 (category Lo: Letter, other)

julia> '基'
'基': Unicode U+57fa (category Lo: Letter, other)

julia> 

中文字符的 Unicode 代碼點比的 Unicode 代碼點要小。所以,"Julia 編程入門"一定會小於"Julia 編程基礎"

如果你有興趣,還可以使用LegacyStrings.jl程序包中的函數來生成採用 UTF-16 或 UTF-32 編碼的字符串值,然後再比較(甚至混合比較)它們。比較結果肯定同樣符合上述的規則。

6.4 非常規的字符串值

如果一個字符串值不僅包含了由雙引號包裹的字符串,還包含了某個特定的前綴,那麼我們就說這個字符串值是非常規的。

6.4.1 原始字符串

我們爲了表示字符串值而輸入的內容又被稱爲字符串字面量。在一般情況下,字符串值的實際內容會與我們爲此輸入的字符串字面量保持一致。例如:

julia> "Julia\n\n"
"Julia\n\n"

julia> 

除非其中包含了非經典的轉義序列或者插值部分。注意,雖然我們輸入的經典轉義序列會被原樣保留在字符串值中,但當該值被打印的時候這些轉義序列還是會被轉義。比如:

julia> println("Julia\n\n")
Julia



julia> 

顯然,當上面這個字符串值被打印時,在打印出的內容的最後有兩個真正的換行。

如果我們想讓一個字符串值被打印出的內容與我們爲它輸入的字符串字面量完全相同,那麼就可以使用原始字符串的形式來表示它。在這種情況下,即使字符串字面量中包含了任意的轉義序列和插值部分,這種一致性也是可以得到保障的。

原始字符串的形式是由前綴raw和常規的字符串值組成的,如:raw"Julia\n\n"。這種形式會生成常規的字符串值。但不同的是,我們輸入的所有內容最終都會保持原樣,包括$\。示例如下:

julia> raw"Julia\n\n"
"Julia\\n\\n"

julia> 

不要被上面回顯的內容所迷惑。其中的\\n實際上就代表了內容\n。這是因爲,在常規的字符串值中,\n是會被轉義爲真正的換行的。所以 Julia 在它的前面又加了一個\,以表示第二個反斜槓代表的並不是轉義序列的前綴。

我們把上面的字符串值打印出來看一下就清楚了:

julia> println(raw"Julia\n\n")
Julia\n\n

julia> 

總之,原始字符串的形式會讓一個字符串值的最終輸出與最初輸入保持一致。爲此,Julia 可能會對字符串值的內容稍加修改。

6.4.2 整數和浮點數

我們在上一章講過,一個常規的字符串值再加上一個前綴big就可以代表任意精度的(BigInt類型的)整數值或者(BigFloat類型的)浮點數值。但前提是,在兩個雙引號之間的必須是有效的整數字面量或者浮點數字面量。例如:

julia> big"1314"
1314

julia> typeof(ans)
BigInt

julia> big"3.14"
3.140000000000000000000000000000000000000000000000000000000000000000000000000008

julia> typeof(ans)
BigFloat

julia> 

注意,如果要用科學計數法表達浮點數,那麼我們只能使用字母e,而不能用fp,否則Julia 就會報錯。示例如下:

julia> big"3.14e-2"
0.03140000000000000000000000000000000000000000000000000000000000000000000000000008

julia> big"3.14f-2"
ERROR: ArgumentError: invalid number format 3.14f-2 for BigInt or BigFloat
Stacktrace:
 [1] top-level scope at none:0

julia> big"3.14p-2"
ERROR: ArgumentError: invalid number format 3.14p-2 for BigInt or BigFloat
Stacktrace:
 [1] top-level scope at none:0

julia> 

另外還要注意,雖然我們可以在這裏使用三聯雙引號,但是並不建議這樣做。因爲這麼寫沒有明顯的好處,而且容易因失誤而輸入無效的字面量。比如:

julia> big"""3.14e-2"""
0.03140000000000000000000000000000000000000000000000000000000000000000000000000008

julia> big"""3.14
       e-2"""
ERROR: ArgumentError: invalid number format 3.14
e-2 for BigInt or BigFloat
Stacktrace:
 [1] top-level scope at none:0

julia> 

6.4.3 版本號

我們已經知道,Julia 的版本號遵循 Semantic Versioning 規範。其一般形式是vX.Y.Z。其中的X代表主版本號(或稱大版本號),Y代表次版本號(或稱小版本號),而Z則代表修訂版本號。並且,它們都只能是正整數或0

在 Julia 程序中,這樣的版本號可以由一種非常規的字符串值表示。其形式是,以字母v作爲前綴,再加上一個內容符合上述規範的字符串字面量。比如,v"1.3.0",我們可以稱之爲版本號值。

在這樣的版本號值中,次版本號和修訂版本號都可以被省略,並且被省略的部分將會被視爲0。因此,v"1.3"就相當於v"1.3.0",而v"1"就相當於v"1.0.0",等等。

另一方面,我們還可以在版本號值中追加更多的信息,包括:預發佈信息和構建信息。預發佈信息實際上指的是那些非穩定版本的信息。比如,我們通常在正式發佈穩定版本1.0.0之前還會發布一系列用於測試或候選的非穩定版本。這些非穩定版本的信息肯定需要體現在對應的版本號中。此類信息可以是-alpha1-beta.2等等。而構建信息表達的是程序構建時處於或針對的環境。它可以是程序構建的日期,也可以是程序當次構建所針對的計算平臺(包括操作系統和計算架構),比如+20200101+win64等等。

預發佈信息的格式爲,一個減號-再加一個預發佈標識,且減號可以被省略。其中的預發佈標識可以包含一到多個小寫的英文字母、09的數字、減號-和英文點號.。但是,英文點號不能作爲開頭或結尾,且多個英文點號不能相鄰。另外,當最開始的減號被省略時,預發佈標識中的第一個字符還不能是數字,否則就可能會引起歧義,從而導致版本號的識別錯誤。例如,預發佈標識爲alphaalpha1alpha.1-alpha.11a都是可以,但.1a1..a卻都是不合法的。又例如,當版本號值是v"1.0.01a"時,修訂版本號會被識別爲01,而預發佈信息會被識別爲a。這與我們想表達的預發佈信息(即1a)並不相符。

按照一般的慣例,alphabetarc都常被用作預發佈標識的前綴,並分別代表內部測試版、公共測試版和候選版。

構建信息的格式是,一個加號+再加一個構建標識。構建標識同樣可以包含一到多個小寫的英文字母、09的數字、減號-和英文點號.,而且對英文點號的用法限制也和預發佈標識是一樣的。因此,我們在這裏放置某種日期時間的簡化表示、哈希序列以及計算平臺的代號等都是沒問題的。

除了上述的規範格式之外,Julia 中的版本號還可以包含兩個特殊的標記。其中一個標記是單獨的減號-。它的存在有個前提條件,即:版本號中不能包含預發佈信息和構建信息。在此條件下,我們可以用這個標記作爲版本號的後綴,以指代某個特定版本的下限。例如,v"1.0.0-"一定會比穩定版本v"1.0.0"以及諸如1.0.0-alpha1.0.0-beta1這樣的非穩定版本都要小。

另一個特殊標記是單獨的加號+。它的存在也有一個前提條件,那就是:版本號中不能包含構建信息。在這個條件下,我們可以用這個標記作爲版本號的後綴,以指代某個特定版本的上限。例如,v"1.0.0+"一定會比v"1.0.0"v"1.0.0+win64"都要大。

請注意,包含了這兩個特殊標記(之一)的版本號無法表示任何具體的版本。但它們對於版本號的比較操作來說還是很有用的。另外,這兩個特殊標記不能出現在同一個版本號值中。

版本號的比較

常量VERSION代表着當前 Julia 語言的版本號。與其他的版本號值一樣,它是VersionNumber類型的。這個類型的值是可以被比較的。我們之前講到的所有比較操作符都可以應用在它們身上。

不過,針對這類值的比較操作有些特殊。它不是單純地按照數值順序或字典順序進行的。在比較此類值的時候,Julia 會先以數值順序依次地比較它們的主版本號、次版本號和修訂版本號。如果這三者都兩兩相等,那麼 Julia 就會去比較它們的預發佈信息。在其他部分都相等的情況下,有預發佈信息的版本號值一定會比沒有該信息的版本號值要小。

預發佈信息會被其中的英文點號分割爲多個單元。這些單元會以從左到右的順序被成對地比較。對於每對單元,如果其中都只包含數字字符,那麼 Julia 就會以數值順序比較它們,如:v"1.0.0-alpha.9"會小於v"1.0.0-alpha.11"。否則,Julia 就會以 ASCII 編碼集的順序逐個字符地進行比較,如:v"1.0.0-alpha.a9"會大於v"1.0.0-alpha.a11"。一旦分辨出某對單元誰大誰小,也就可以確定兩個預發佈信息的大小了。但如果所有成對的單元都相等,那麼就要看哪一個預發佈信息擁有更少的單元了。在這時,更少的單元意味着更大的值。

版本號值中的構建信息也會在最後參與比較。它的比較規則與預發佈信息的比較規則基本一致。唯一不同的是,在其他部分都相等的情況下,有構建信息的版本號值一定會比沒有該信息的版本號值要大。

有了以上這些規則,再結合我們剛剛在前面說的那兩個特殊標記,就有了下面的關係:

julia> v"1.0.0-" < v"1.0.0-alpha" < v"1.0.0-alpha.9" < v"1.0.0-alpha.11" < v"1.0.0-alpha.a11" < v"1.0.0-alpha.a9" < v"1.0.0-alpha.a9.1" < v"1.0.0-beta" < v"1.0.0-beta.2" < v"1.0.0-rc.1" < v"1.0.0" < v"1.0.0+win64" < v"1.0.0+"
true

julia> 

6.4.4 正則表達式

所謂的正則表達式(regular expressions),就是使用一系列的符號來表達字符串的特定模式的公式。它常常被用來檢索或替換那些符合某個特定模式的字符串片段,又或是用於判斷一個字符串是否符合某些特定的模式。注意,這遠遠要比在一個字符串中搜索某個固定的字符串片段要複雜得多。

Julia 的正則表達式其實是一個舶來品,傳承自 Perl 語言。Perl 是一種用於編寫腳本程序的編程語言,誕生於 1987 年。該語言內置的正則表達式引擎在功能上非常的強大,而且算是一個集大成者。它也因此一度成爲了業界標準。

在底層,Julia 的正則表達式是由 PCRE 庫支持的。PCRE 是 Perl Compatible Regular Expressions 的縮寫。它使用了與 Perl 5 幾乎相同的語法和語義來實現正則表達式的模式匹配。更確切地說,Julia 使用的是 PCRE 庫的新實現,名爲 PCRE2。這個新實現誕生於 2015 年,目前已經發展到了第10個版本。

實際上,Julia 在識別由字符串值代表的版本號時就用到了正則表達式。我們可以利用函數match和代表了正則表達式的常量Base.VERSION_REGEX來判斷一個版本號的格式是否符合規範。例如:

julia> match(Base.VERSION_REGEX, "1.0.0-rc1+win64")
RegexMatch("1.0.0-rc1+win64", 1="1", 2="0", 3="0", 4=nothing, 5="-rc1", 6=nothing, 7="win64")

julia> match(Base.VERSION_REGEX, "1.0.0-rc1_")

julia> ans == nothing
true

julia> 

如果符合規範,那麼match函數就會返回一個RegexMatch類型的值,否則它就會返回nothing。根據 REPL 環境回顯的內容可知,match函數已經識別出了版本號"1.0.0-rc1+win64"中的各個組成部分。

我在這裏不想過多地介紹正則表達式的語法和用法。因爲系統的介紹會佔用非常大的篇幅,足以寫成一本書了。實際上,目前市面上已經有不少介紹正則表達式的圖書了。如果有必要,你可以挑選一本來閱讀,也可以去參看 PCRE2 官方網站上的語法文檔模式文檔

我下面只從非常規字符串值的角度,說一下正則表達式的一般表示形式和基本操作。

這種非常規的字符串值由前綴r和包含了正則表達式的字符串字面量組成,以下簡稱正則值。正則值的類型總是Regex。比如,正則值r"^(\d+)$"可以匹配只包含了一個或多個數字字符的單行字符串。又比如,正則值r"\+((?:[0-9a-z-]+\.)*[0-9a-z-]+)"可用於匹配版本號中的構建信息。

我們現在來簡單地拆解一下上面的第二個正則表達式。首先是轉義序列\+。這是在正則表達式中特有的轉義序列。它表達的含義是,這裏的加號+只是一個普通的字符,而不是用於指示匹配次數的量詞(quantifier)。類似的轉義序列還有\.\*\(等等。

緊隨其後的是一個捕獲組(capture group),即:由圓括號包裹的子表達式。它可以實現兩個功能:分組和捕獲。說明如下:

  • 分組功能:可以把捕獲組中的子表達式看成一個獨立的整體。使它可以獨立匹配字符串片段,並能成爲一些符號(比如量詞)的作用對象。比如,(-|\+)?([0-9]+)+可以匹配代表整數的字符串。其中,第 1 個捕獲組可以獨立匹配正負號,同時也是量詞?的作用對象並以此表示正負號可有可無。而第 2 個捕獲組可以獨立匹配數字字符,同時也是量詞+的作用對象並以此表示數字字符至少要有一個。
  • 捕獲功能:可以提取出捕獲組中的子表達式,以便在後續引用。比如,(-|\+)?([0-9]+)+\.(\g<2>)+可以匹配代表小數的字符串。其中,第 3 個捕獲組中的\g<2>的含義就是引用第 2 個捕獲組中的子表達式,以表示小數部分的模式與整數部分的模式相同。

我們接着拆解可以匹配構建信息的那個正則表達式。在緊隨轉義序列\+的那個捕獲組中,還有兩個獨立的子表達式。

第一個子表達式是(?:[0-9a-z-]+\.)*,是一個非捕獲組(non-capture group)。非捕獲組的含義是隻有分組功能而沒有捕獲功能的組,一般以(?:爲前綴且以)爲後綴。在這個非捕獲組中的[0-9a-z-]+表示至少要有一個09的數字、小寫英文字母或減號-。而\.則表示前者可以以英文點號.爲後綴。最後的量詞*表示這個非捕獲組所表達的字符串片段可以有零個到多個。

如果你理解了第一個子表達式,那麼再看第二個子表達式[0-9a-z-]+肯定就毫無阻礙了。這兩個子表達式合在一起就形成了外層捕獲組的子表達式。它表示了構建信息本身的模式。再加上最左側的轉移序列\+,這個正則表達式就可以識別出合法的構建信息並提取出構建信息本身了。就像下面這樣:

julia> rm1 = match(r"\+((?:[0-9a-z-]+\.)*[0-9a-z-]+)", "+win64.20200101")
RegexMatch("+win64.20200101", 1="win64.20200101")

在 REPL 環境的回顯內容中,跟在RegexMatch(後邊的"+win64.20200101"就是已被成功識別的構建信息。而1="win64.20200101"則表示第 1 個捕獲組匹配的字符串是"win64.20200101"

在 Julia 程序中,我們可以通過訪問RegexMatch類型值的一些字段來了解匹配結果的具體細節。這些字段有:

  • match:代表匹配到的整個字符串。
  • captures:代表所有捕獲組匹配到的字符串片段,會以字符串數組的形式表示,並以捕獲組的序號爲順序。
  • offset:代表匹配到的整個字符串在被匹配的完整字符串中的偏移量,可以理解爲前者在後者中的首個字符索引號。
  • offsets:代表所有捕獲組匹配到的字符串片段在被匹配的完整字符串中的偏移量,會以整數數組的形式表示,並以捕獲組的序號爲順序。
  • regex:代表匹配時所使用的正則值。

相關的示例如下:

julia> rm1.match
"+win64.20200101"

julia> rm1.captures
1-element Array{Union{Nothing, SubString{String}},1}:
 "win64.20200101"

julia> rm1.offset
1

julia> rm1.offsets
1-element Array{Int64,1}:
 2

julia> rm1.regex
r"\+((?:[0-9a-z-]+\.)*[0-9a-z-]+)"

julia> 

除了match函數,正則值還可以作爲occursin函數的第一個參數值,以及作爲replace函數的第二個參數值。

利用replace函數和正則值,我們可以對字符串值的內容進行一些複雜的修改和替換(當然,這會生成新的字符串值,而原字符串值會保持不變)。比如:

julia> replace("+win64.2020-01-01T21:01", r"(.*\.)(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2})" => s"\1\2\3\4\5\6")
"+win64.202001012101"

julia> 

s爲前綴的非常規字符串值專門用於表示替換字符串(substitution string),以下簡稱替換值。替換值的類型總是SubstitutionString。在這裏,我用正則值、符號=>和替換值組成了一個替換對,以表示:把與該正則值相匹配的字符串替換爲該替換值中的內容。在這個替換值中,我們可以使用\g<n>\n來引用正則值中的捕獲組,其中的n代表捕獲組的序號。因此,我用"\1\2\3\4\5\6"重新組織了源字符串值中的內容。

對於正則值,除了必要的前綴r,我們還可以爲它添加後綴imsx。這些後綴的含義如下:

  • i:在進行模式匹配時不區分大小寫。這會依從於相應編碼標準中的規則。最簡單的案例是,不區分某一個英文字母的大寫和小寫,把兩者視爲同一個字符。
  • m:將源字符串視爲多行的字符串值。也就是說,修改原本指代字符串最前端的^和指代字符串最後端的$的含義,分別改爲指代任何行的最前端和指代任何行的最後端。如此一來,我們就可以分別針對源字符串中的每一行做模式匹配了。
  • s:將源字符串視爲單行的字符串值。也就是說,將原本指代了除換行符以外的任何字符的.的含義改爲可指代所有字符。這樣我們就可以針對源字符串的全範圍做模式匹配了,即使它擁有多個行也是如此。
  • x:允許我們在正則表達式中的某些位置上添加一些空白,甚至是換行。這可以提高正則表達式的(人類)可讀性。

下面的示例有助於你理解這些後綴的含義。

julia> match(r"^J\w+$", "julia") # 區分大小寫的匹配。

julia> match(r"^J\w+$"i, "julia") # 不區分大小寫的匹配。
RegexMatch("julia")

julia> match(r"^J\w+$", "Julia\n Python\n Golang\n") # 未改變 ^ 和 $ 的含義。

julia> match(r"^J\w+$"m, "Julia\n Python\n Golang\n") # 已改變 ^ 和 $ 的含義,可針對每一行做匹配。
RegexMatch("Julia")

julia> match(r"J.*", "Julia\n Python\n Golang\n") # 未改變 . 的含義。
RegexMatch("Julia")

julia> match(r"J.*"s, "Julia\n Python\n Golang\n") # 已改變 . 的含義,可匹配換行。
RegexMatch("Julia\n Python\n Golang\n")

julia> match(r"^ J  \w+ $", "Julia") # 正則表達式中不能有多餘的空白。

julia> match(r"^ J  \w+ $"x, "Julia") # 正則表達式中可以有多餘的空白。
RegexMatch("Julia")

julia> 

最後,順便說一下,我們可以使用三聯雙引號來包裹正則值中的字符串字面量。在某些情況下,這樣做可以讓正則表達式的內容更加清晰。比如:

julia> match(r"^ \"J\w+\" $", """ "Julia" """)
RegexMatch(" \"Julia\" ")

julia> match(r"""^ "J\w+" $""", """ "Julia" """)
RegexMatch(" \"Julia\" ")

julia> 

可以看到,在用了三聯雙引號之後,我們就不需要再爲正則表達式中的雙引號做轉義了。

6.4.5 字節數組

字節數組也可以由一種非常規的字符串值表示。但這樣表示的字節數組是隻讀的。這種字節數組的類型是Base.CodeUnits{UInt8, String}。例如:

julia> b"abcdef"
6-element Base.CodeUnits{UInt8,String}:
 0x61
 0x62
 0x63
 0x64
 0x65
 0x66

julia>

我用字符串值b"abcdef"生成了一個長度爲6的字節數組。這個字節數組中的每一個元素值都表示了"abcdef"經編碼後在對應字節上的存儲內容。更確切地說,Julia 會先用 UTF-8 編碼格式把字符串值中的內容轉換成一個個字節,然後再把這些字節按照先後順序保存到一個字節數組當中。

在這種非常規的字符串值中,我們可以使用任何有效的形式來表示一個 ASCII 編碼值或者一個 Unicode 代碼點。比如:

julia> ba1 = b"\u4e2d\xe5\x9b\xbd"
6-element Base.CodeUnits{UInt8,String}:
 0xe4
 0xb8
 0xad
 0xe5
 0x9b
 0xbd

julia> String(ba1)
"中國"

julia>

關於這些表示形式的細節,我們在前面已經討論過了。我就不在此重複了。另外,我們還沒有正式講數組和它的類型,所以我在這裏並不打算展開來說。你目前只需要知道,有這樣一種非常規的字符串值,它能夠表示只讀的字節數組。

6.5 小結

我們在本章主要講解了字符和字符串。這兩者都可以表示處於 Unicode 代碼空間中的字符。但不同的是,前者只能表示一個字符,而後者可以表示多個字符。

我們首先簡要地介紹了 ASCII 編碼和 Unicode 編碼標準,並提及了後者中的一種編碼格式:UTF-8。Julia 通常採用 UTF-8 編碼格式把字符轉換爲由若干個字節承載的二進制數。

然後,我們講述了 Julia 中的字符值。這包括它的表示與操作和它的類型與轉換方法。多個字符可以組成一個字符串。所以我們緊接着又講了字符串值的表示以及在其類型之上的設定。這些設定是我們操作字符串值的基礎。我們可以對字符串值做的操作有,獲取長度、索引、截取、拼接、插值,以及搜索和比較。

除了常規的字符串值,我們還可以利用簡單的前綴編寫非常規的字符串值,以表示某類特殊值。比如,原始字符串、任意精度的整數和浮點數、版本號、正則表達式,以及只讀的字節數組。在某些場景下,這些特殊值是非常有用的。

字符和字符串是我們在 Julia 編程過程中非常常用的兩類值。它們的表示方式頗多,且操作方法多樣。我們往往需要根據具體情況對它們加以合理的運用。最後再強調一下,字符值和字符串值都是不可變的!

系列文章:

Julia編程基礎(一):初識Julia,除了性能堪比C語言還有哪些特性?

Julia編程基礎(二):開發Julia項目必須掌握的預備知識

Julia編程基礎(三):一文掌握Julia的變量與常量

Julia 編程基礎(四):如何用三個關鍵詞搞懂 Julia 類型系統

Julia編程基礎(五):數值與運算

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章