Rust 數據結構:String
- Rust 數據結構:String
- 什么是字符串?
- 創建新字符串
- 更新字符串
- 將 push_str 和 push 附加到 String 對象后
- 使用 + 運算符和 format! 宏
- 索引到字符串
- 字符串在內存中的表示
- 字節、標量值和字形簇
- 分割字符串
- 遍歷字符串的方法
Rust 數據結構:String
在本文中,我們將討論每種集合類型都具有的 String 操作,例如創建、更新和讀取。我們還將討論 String 與其他集合的不同之處,即由于人和計算機解釋 String 數據的方式不同,對 String 進行索引變得復雜。
什么是字符串?
Rust 在核心語言中只有一種字符串類型,它是字符串切片 str,通常以它的借用形式 &str 出現。字符串切片是對存儲在其他地方的一些 UTF-8 編碼字符串數據的引用。例如,字符串字面值存儲在程序的二進制文件中,因此是字符串切片。
String 類型是由 Rust 的標準庫提供的,而不是編碼到核心語言中,它是一個可增長的、可變的、擁有的、UTF-8 編碼的字符串類型。
當在 Rust 使用“字符串”時,它們可能指的是 String 或 String slice &str 類型,而不僅僅是其中一種類型。雖然本文主要是關于 String 的,但這兩種類型在 Rust 的標準庫中都大量使用,并且 String 和 String 切片都是 UTF-8 編碼的。
創建新字符串
String 實際上是作為字節向量的包裝器實現的,帶有一些額外的保證、限制和功能,所以在使用上很多和 vector 類似。
let mut s = String::new();
這一行創建了一個新的空字符串 s,然后我們可以將數據加載到其中。
通常,我們會有一些初始數據,我們想用這些數據開始字符串。為此,我們使用 to_string 方法,該方法可用于任何實現 Display trait 的類型,就像字符串字面量一樣。
let data = "initial contents";let s = data.to_string();// The method also works on a literal directly:let s = "initial contents".to_string();
還可以使用 String::from 函數從字符串字面值創建 String。
let s = String::from("initial contents");
更新字符串
String 的大小可以增長,其內容可以改變。
將 push_str 和 push 附加到 String 對象后
push_str 方法接受一個字符串切片,并且不獲取參數的所有權。
let mut s = String::from("foo");s.push_str("bar");
push 方法接受單個字符作為參數,并將其添加到 String 中。
let mut s = String::from("lo");s.push('l');
使用 + 運算符和 format! 宏
+ 操作符可以組合兩個現有字符串。
let s1 = String::from("Hello, ");let s2 = String::from("world!");let s3 = s1 + &s2; // note s1 has been moved here and can no longer be used
s1 在相加之后不再有效的原因,以及我們使用 s2 引用的原因,與使用 + 操作符時調用的方法的簽名有關。+ 操作符使用 add 方法,其簽名看起來像這樣:
fn add(self, s: &str) -> String {
首先,s2 有一個 &,這意味著我們將第二個字符串的引用添加到第一個字符串。
我們能夠在 add 調用中使用&s2(String 類型)的原因是編譯器可以將 &String 實參強制轉換為 &str。當我們調用 add 方法時,Rust 使用了一個強制轉換,它將 &s2 轉換為 &s2[…]。因為 add 沒有獲得 s 形參的所有權,所以在這個操作之后 s2 仍然是一個有效的 String。
其次,我們可以在簽名中看到 add 取得了 self 的所有權,因為 self 沒有 &。這意味著 s1 將被移動到 add 調用中,并且在此之后將不再有效。
綜上,s3 = s1 + &s2;
看起來它將復制兩個字符串并創建一個新字符串,這個語句實際上獲取 s1 的所有權,附加 s2 內容的副本,然后返回結果的所有權。
如果需要連接多個字符串,則 + 操作符的行為會變得笨拙:
let s1 = String::from("tic");let s2 = String::from("tac");let s3 = String::from("toe");let s = s1 + "-" + &s2 + "-" + &s3;
對于以更復雜的方式組合字符串,我們可以使用 format! 宏:
let s1 = String::from("tic");let s2 = String::from("tac");let s3 = String::from("toe");let s = format!("{s1}-{s2}-{s3}");
format! 宏返回一個包含內容的 String。format! 宏使用引用,因此此調用不會獲得其任何參數的所有權。
索引到字符串
在許多其他編程語言中,通過索引引用字符串中的單個字符是一種有效且常見的操作。但是,如果嘗試在 Rust 中使用索引語法訪問 String 的某些部分,則會得到一個錯誤。
let s1 = String::from("hi");let h = s1[0];
報錯:error[E0277]: the type `str` cannot be indexed by `{integer}`
這個要從 Rust 如何在內存中 存儲字符串開始講起。
字符串在內存中的表示
String是Vec<u8>的包裝器。
考慮以下兩個字符串:
let s1 = String::from("Hola");
let s2 = String::from("Здравствуйте");
s1 的長度是 4 字節。當用 UTF-8 編碼時,這些字母中的每個都占 1 字節。然而,s2 的長度不是 12 字節,而是 24 字節。因為該字符串中的每個 Unicode 標量值需要 2 字節的存儲空間。
來看一下錯誤代碼:
let hello = "Здравствуйте";
let answer = &hello[0];
當用 UTF-8 編碼時,З 的第一個字節是 208,第二個字節是 151,所以看起來答案實際上應該是 208,但是 208 本身并不是一個有效的字符。為了避免返回意外值并導致可能無法立即發現的錯誤,Rust 根本不編譯此代碼。
字節、標量值和字形簇
關于 UTF-8 的另一點是,從 Rust 的角度來看,實際上有三種相關的方式來看待字符串:字節、標量值和字形簇(最接近我們稱之為字母的東西)。
如果我們看看寫在 Devanagari 腳本中的印地語單詞 “??????”,它被存儲為 u8 值的向量,看起來像這樣:
[224, 164, 168, 224, 164, 174, 224, 164, 184, 224, 165, 141, 224, 164, 164,
224, 165, 135]
這是 18 個字節,這就是計算機最終存儲這些數據的方式。如果我們把它們看作 Unicode 標量值,也就是 Rust 的 char 類型,這些字節看起來是這樣的:
['?', '?', '?', '?', '?', '?']
Rust 提供了不同的方式來解釋計算機存儲的原始字符串數據,這樣每個程序都可以選擇它需要的解釋,而不管這些數據是什么人類語言。
Rust 不允許我們索引 String 以獲取字符的最后一個原因是,索引操作總是需要常數時間(O(1))。但是不能保證 String 的性能,因為 Rust 必須從頭到尾遍歷內容,以確定有多少個有效字符。
分割字符串
對字符串進行索引通常不是一個好主意,與其用 [] 索引單個數字,不如用 [] 索引一個范圍來創建包含特定字節的字符串切片。
let hello = "Здравствуйте";let s = &hello[0..4];
這里,s 將是一個 &str,它包含字符串的前 4 個字節。前面,我們提到每個字符都是兩個字節,這意味著 s 將是 “Зд”。
如果我們嘗試用 &hello[0…1], Rust 會在運行時報錯,就像在 vector 中訪問無效索引一樣。
遍歷字符串的方法
對字符串片段進行操作的最佳方法是明確說明是需要字符還是字節。對于單個 Unicode 標量值,使用 chars 方法。在 “Зд” 上調用 chars,分離并返回兩個 char 類型的值,再遍歷。
for c in "Зд".chars() {println!("{c}");
}
程序輸出:
З
д
或者,bytes 方法返回每個原始字節。
for b in "Зд".bytes() {println!("{b}");
}
程序輸出:
208
151
208
180
一定要記住,有效的 Unicode 標量值可能由多個字節組成。