跳到內容

字串

在先前的課程中,我們已經熟悉了大多數程式的主要組成部分:字串。讓我們回顧一下基本屬性

字串是編碼為 UTF-8Unicode 字元的序列。字串是不可變的:如果您對字串應用修改,您實際上會得到一個具有修改內容的新字串。原始字串保持不變。

字串通常以雙引號字元 (") 括起來的字面值形式寫入。

插值

字串插值是組合字串的便捷方法:字串字面值內的 #{...} 會在字串的這個位置插入大括號之間的表達式的值。

name = "Crystal"
puts "Hello #{name}"

插值內的表達式應保持簡短,僅限於變數或簡單的方法呼叫。較複雜的表達式會降低程式碼的可讀性。

表達式的值不一定要是字串。任何型別都可以,並且會透過呼叫 #to_s 方法轉換為字串表示。此方法為任何物件定義。讓我們嘗試使用數字

name = 6
puts "Hello #{name}!"

注意

插值的替代方法是串連。您可以撰寫 "Hello " + name + "!",而不是 "Hello #{name}!"。但是這樣會比較笨重,而且對於非字串型別會有一些陷阱。一般來說,插值比串連更受歡迎。

跳脫

有些字元無法直接在字串字面值中寫入。例如,雙引號:如果在字串內使用,編譯器會將其解譯為結束分隔符號。

這個問題的解決方案是跳脫:如果雙引號前面有一個反斜線 (\),則會將其解譯為跳脫序列,並且兩個字元一起編碼為雙引號字元。

puts "I say: \"Hello World!\""

還有其他的跳脫序列:例如,換行符號 (\n) 或製表符號 (\t) 等不可列印的字元。如果您想要寫入字面反斜線,則跳脫序列為雙反斜線 (\\)。空字元 (碼位 0) 是 Crystal 字串中的一般字元。在某些程式語言中,此字元表示字串的結尾。但是在 Crystal 中,它僅由其 #size 屬性決定。

puts "I say: \"Hello \\\n\tWorld!\""

提示

您可以在字串字面值參考中找到關於可用跳脫序列的更多資訊。

替代分隔符號

有些字串字面值可能包含許多雙引號 – 例如,想想具有引號引數值的 HTML 標籤。必須使用反斜線跳脫每一個引號會很麻煩。替代字面值分隔符號是一個方便的替代方法。%(...) 等效於 "...",只是分隔符號以括號 (()) 而不是雙引號表示。

puts %(I say: "Hello World!")

跳脫序列和插值仍然以相同的方式運作。

提示

您可以在字串字面值參考中找到關於替代分隔符號的更多資訊。

Unicode

Unicode 是一種國際標準,用於表示許多不同書寫系統中的文字。除了英文和許多其他語言使用的拉丁字母之外,它還包含許多其他字元集。Unicode 標準不僅適用於純文字,還包含表情符號和圖示。

以下範例使用 Unicode 字元 U+1F310 (地球與經緯線) 來向世界發送訊息

puts "Hello 🌐"

處理 Unicode 符號有時可能會有點棘手。某些字元可能不受您的編輯器字體支援,某些字元甚至無法列印。作為替代方法,Unicode 字元可以用跳脫序列表示。反斜線後跟字母 u 表示 Unicode 碼位。碼位值以十六進位數字寫入,並以大括號括住。如果碼位正好有四個數字,則可以省略大括號。

puts "Hello \u{1F310}"

轉換

假設您想要變更字串的某些內容。也許要大聲說出訊息並將其全部設為大寫?String#upcase 方法會將所有小寫字元轉換為它們的大寫等效字元。相反的方法是 String#downcase。還有一些類似的方法,可讓我們以不同的樣式表達我們的訊息

message = "Hello World! Greetings from Crystal."

puts "normal: #{message}"
puts "upcased: #{message.upcase}"
puts "downcased: #{message.downcase}"
puts "camelcased: #{message.camelcase}"
puts "capitalized: #{message.capitalize}"
puts "reversed: #{message.reverse}"
puts "titleized: #{message.titleize}"
puts "underscored: #{message.underscore}"

#camelcase#underscore 方法不會變更這個特定的字串,但是您可以嘗試使用 "snake_cased""CamelCased" 輸入。

資訊

讓我們更詳細地了解字串以及我們可以知道的內容。首先,字串有一個長度,即它包含的字元數。此值以 String#size 提供。

message = "Hello World! Greetings from Crystal."

p! message.size

若要判斷字串是否為空,您可以檢查大小是否為零,或者只使用簡寫 String#empty?

empty_string = ""

p! empty_string.size == 0,
  empty_string.empty?

如果字串為空,或者僅包含空白字元,則 String#blank? 方法會傳回 true。相關的方法是 String#presence,如果字串為空白,則會傳回 nil,否則會傳回字串本身。

blank_string = ""

p! blank_string.blank?,
  blank_string.presence

相等與比較

您可以使用相等運算子 (==) 測試兩個字串是否相等,並使用比較運算子 (<=>) 比較它們。兩者都會嚴格地逐字元比較字串。請記住,<=> 會傳回一個整數,表示兩個運算元之間的關係,如果比較結果為 0,則 == 會傳回 true,也就是說,兩個值比較結果相等。

但是,也有一個 #compare 方法,提供不區分大小寫的比較。

message = "Hello World!"

p! message == "Hello World",
  message == "Hello Crystal",
  message == "hello world",
  message.compare("hello world", case_insensitive: false),
  message.compare("hello world", case_insensitive: true)

部分組成

有時候,我們並不需要知道字串是否完全匹配,而只是想知道一個字串是否包含另一個字串。例如,讓我們使用 #includes? 方法來檢查訊息是否與 Crystal 有關。

message = "Hello World!"

p! message.includes?("Crystal"),
  message.includes?("World")

有時,字串的開頭或結尾會特別重要。這時,#starts_with?#ends_with? 方法就派上用場了。

message = "Hello World!"

p! message.starts_with?("Hello"),
  message.starts_with?("Bye"),
  message.ends_with?("!"),
  message.ends_with?("?")

子字串索引

我們可以透過 #index 方法取得關於子字串位置更詳細的資訊。它會回傳子字串首次出現時,第一個字元的索引值。結果 0 的意義與 starts_with? 相同。

p! "Crystal is awesome".index("Crystal"),
  "Crystal is awesome".index("s"),
  "Crystal is awesome".index("aw")

這個方法有一個可選的 offset 引數,可用於從字串開頭以外的不同位置開始搜尋。當子字串可能多次出現時,這會很有用。

message = "Crystal is awesome"

p! message.index("s"),
  message.index("s", offset: 4),
  message.index("s", offset: 10)

#rindex 方法的作用相同,但它是從字串的結尾開始搜尋。

message = "Crystal is awesome"

p! message.rindex("s"),
  message.rindex("s", 13),
  message.rindex("s", 8)

如果沒有找到子字串,結果會是一個稱為 nil 的特殊值。它的意思是「沒有值」。當子字串沒有索引時,這是合理的。

查看 #index 的回傳型別,我們可以看到它會回傳 Int32Nil

a = "Crystal is awesome".index("aw")
p! a, typeof(a)
b = "Crystal is awesome".index("meh")
p! b, typeof(b)

提示

我們將在下一課更深入地探討 nil

提取子字串

子字串是字串的一部分。如果您想提取字串的部分內容,可以使用幾種方法。

索引存取器 #[] 允許透過字元索引和大小來參照子字串。字元索引從 0 開始,到長度(即 #size 的值)減一結束。第一個引數指定子字串中第一個字元的索引,第二個引數指定子字串的長度。 message[6, 5] 會提取一個從索引六開始,長度為五個字元的子字串。

message = "Hello World!"

p! message[6, 5]

假設我們已經確定字串以 Hello 開頭並以 ! 結尾,並且想要提取中間的內容。如果訊息是 Hello Crystal,我們不會得到完整的單字 Crystal,因為它長度超過五個字元。

一個解決方案是從整個字串的長度減去開頭和結尾的長度,來計算子字串的長度。

message = "Hello World!"

p! message[6, message.size - 6 - 1]

有一個更簡單的方法可以做到這一點:索引存取器可以與字元索引的 Range 一起使用。範圍字面值由一個起始值和一個結束值組成,以兩個點(..)連接。第一個值表示子字串的起始索引,就像之前一樣,但第二個值是結束索引(與長度相反)。現在我們不需要在計算中重複起始索引,因為結束索引只是大小減二(一個用於結束索引,另一個用於排除最後一個字元)。

它可以更簡單:負索引值會自動關聯到字串的結尾,因此我們不需要明確地從字串大小計算結束索引。

message = "Hello World!"

p! message[6..(message.size - 2)],
  message[6..-2]

替換

以非常相似的方式,我們可以修改字串。讓我們確保我們正確地問候 Crystal,而沒有其他。我們呼叫 #sub 而不是存取子字串。第一個引數同樣是一個範圍,用於指示要被第二個引數的值替換的位置。

message = "Hello World!"

p! message.sub(6..-2, "Crystal")

#sub 方法非常通用,可以用不同的方式使用。我們也可以將搜尋字串作為第一個引數傳遞,它會將該子字串替換為第二個引數的值。

message = "Hello World!"

p! message.sub("World", "Crystal")

#sub 只會替換搜尋字串的第一個實例。它的老大哥 #gsub 則會應用於所有實例。

message = "Hello World! How are you, World?"

p! message.sub("World", "Crystal"),
  message.gsub("World", "Crystal")

提示

您可以在字串字面值參考String API 文件中找到更詳細的資訊。