字串

字串 在 Julia

7 個練習

關於 字串

在 Julia 中,String是一串有序的字元。正如手冊所指出的:「當然,真正的麻煩在於追問字元究竟是什麼。」

這些複雜的細節大多會在 Chars概念中有更詳細的介紹,不過這裡先簡單說幾點:

  • Julia 中所有字串都是 Unicode。
  • 剛好也屬於 ASCII的字串(大小寫英文字母 A 到 Z、數字,以及少數標點字元)處理起來很_輕鬆_。
  • 世界上大多數其他的書寫系統也_可以_處理,但需要多花點心思。

字串常值

一般來說,字串會用雙引號 " "包住。單引號 ' '只用於 Chars。

也可以使用三個雙引號 """ """,這樣就能在字串裡直接使用 "而不必跳脫。更重要的是,多行字串中不需要的多餘縮排會被去除。

julia> """
       Multiline
       String
       """
"Multiline\nString\n"

跳脫字元

就像從 C 以來的大多數語言一樣,Julia 用反斜線 \來跳脫:

  1. 輸入不會列印出來的字元,例如前一個例子裡的換行 \n。
  2. 保護原本具有特殊意義的字元,例如 \$(見下文)。

疊代

字串是可疊代的集合,所以 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 字串來說,位元組和字元之間並不是 1:1 的關係。一個 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()函式裡。
  • 在 Exercism 測試執行器之外,PyFormattedStrings 套件可以模擬 Python 的 f-string。
  • @sprintf()巨集會在下面說明。

@sprintf()

為了更精細地控制字串的組合,Julia 沿用了 C 的 sprintf 函式(以及之後好幾種語言:維基百科列出了大約 30 種)。

在 Julia 中,這是以 Printf 模組裡的 macro實作的,因此帶有 @ 前綴。

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

要取得子字串的 start:end 索引,我們有幾個函式:

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 "

解析

數字和它的字串表示法之間的互相轉換,往往很有用。

數字轉字串很簡單,不過要用預設值 10 以外的進位基底時,需要多一個參數:

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" "用於 Regex,s" "則用來處理擷取到的片段。

其他各式各樣的特殊字串,在 Exercism 風格的程式設計中比較少見,但在開發函式庫時被廣泛使用。

  • 語意化版本字串 v"x.y.z"用來處理軟體發行版本的 major.minor.patch 編號。
  • 位元組陣列常值 b" "會繞過 Unicode 的字元邊界,直接在位元組層級運作。
透過 GitHub 編輯 連結會在新視窗或分頁中開啟

學習 字串

練習已鎖定

再解鎖 2 個練習,就能練習 字串