本文 AI 產出,尚未審核
Rust 語言教學:字串(String vs &str)
簡介
在 Rust 中,字串是最常見的資料型別之一,幾乎每一個程式都會與文字互動。正確掌握 String 與 &str 的差異與使用時機,不僅能避免記憶體安全的陷阱,還能寫出效能更佳、可讀性更高的程式碼。
本單元聚焦於「基本語法與變數」裡的字串概念,從底層的記憶體模型說起,帶你一步步了解何時應該使用可變長度的 String,何時則適合使用不可變的字串 slice &str。透過實作範例與常見錯誤的解析,你將能在日常開發中自信地選擇最適合的字串型別。
核心概念
1. &str:字串 slice(不可變視圖)
- 本質:
&str是對 UTF-8 編碼資料的不可變參考(slice),類似於其他語言的「字串常量」或「字元陣列」的只讀視圖。 - 記憶體布局:
&str只包含兩個指標:ptr(指向字元資料的起始位址)與len(資料長度,單位是位元組)。它本身不擁有資料,資料的所有權仍屬於其他變數(例如字面值、String、或是從檔案讀取的緩衝區)。 - 編譯期常量:字面值
"Hello"會直接編譯成&'static str,其生命週期是'static,意即程式執行期間都有效。
// &str 範例:字面值是編譯期常量
let greeting: &str = "Hello, Rust!"; // greeting 的型別是 &str
println!("{}", greeting);
2. String:擁有所有權的可變字串
- 本質:
String是一個 擁有所有權 的可變容器,內部以Vec<u8>為底層結構,儲存 UTF-8 編碼的位元組。 - 可變長度:可以使用
push、push_str、+、format!等方法在執行時動態增減內容。 - 所有權轉移:當
String被傳遞給函式或回傳時,所有權會依照所有權規則移動(move)或借用(borrow),這是 Rust 記憶體安全的核心。
// 建立一個可變的 String
let mut msg = String::from("Hello");
msg.push_str(", world"); // 在尾端加入字串
msg.push('!'); // 加入單一字元
println!("{}", msg); // 輸出: Hello, world!
3. 為什麼兩者共存?
- 效能考量:若字串在程式中僅作為只讀參考,使用
&str可避免不必要的記憶體配置與拷貝。 - 所有權需求:若需要在函式內部或跨模組改變字串內容,則必須使用
String,因為只有它擁有資料的所有權,才能安全地修改或釋放。 - API 設計:在公開函式的簽名中,常見的做法是接受
&str(借用)作為參數,讓呼叫端自行決定是否傳入字面值、String的 slice,或其他來源的 slice,提升彈性。
4. 轉換方法
| 從 → 到 | 方法 | 會不會搬移所有權? |
|---|---|---|
&str → String |
to_string()、String::from() |
會(產生新所有權) |
String → &str |
as_str()、&my_string |
不會(僅借用) |
String → &mut str |
as_mut_str()(unstable) |
不會(仍是借用) |
let slice: &str = "固定文字";
let owned: String = slice.to_string(); // 產生新的 String,搬移所有權
let back_slice: &str = &owned; // 再次借用為 &str
println!("owned = {}, back_slice = {}", owned, back_slice);
5. 常見字串操作
5.1 切割與索引
- 切割:
&str支援split,split_whitespace,lines等迭代器。 - 索引:直接以
[i]取字元會 編譯錯誤,因為 UTF-8 的字元長度不固定。必須先取得char或使用bytes()。
let text = "Rust 🦀 語言";
for word in text.split_whitespace() {
println!("word: {}", word);
}
// 取得第 5 個位元組(注意可能不是完整的字元)
let fifth_byte = text.as_bytes()[4];
println!("第五個位元組: {}", fifth_byte);
5.2 拼接
+:消耗左側的String(所有權移動),右側接受&str。format!:不消耗任何參數,回傳新的String。
let hello = String::from("Hello");
let world = "world";
let exclamation = hello + ", " + world + "!"; // hello 已被移動,無法再使用
println!("{}", exclamation);
// 使用 format!,保留所有權
let a = String::from("A");
let b = String::from("B");
let c = format!("{}-{}", a, b); // a、b 仍可使用
println!("c = {}", c);
5.3 替換與刪除
let mut sentence = String::from("I love Rust!");
sentence = sentence.replace("love", "hate"); // 產生新 String,舊的被釋放
println!("{}", sentence); // I hate Rust!
sentence.truncate(7); // 只保留前 7 個位元組
println!("{}", sentence); // I hate
常見陷阱與最佳實踐
| 陷阱 | 說明 | 解決方案 |
|---|---|---|
| 直接以索引取字元 | let c = s[0]; 會編譯錯誤,因 UTF-8 變長 |
使用 s.chars().nth(0) 或 s.as_bytes()[0] |
忘記 mut |
String 本身可變,但若未宣告 mut,所有修改方法會失敗 |
宣告 let mut s = String::new(); |
不必要的 clone() |
在接受 &str 參數時,直接傳入 String 的 slice &my_string,避免 clone() |
fn foo(s: &str) { … } → foo(&my_string); |
| 切片邊界錯誤 | &s[0..2] 可能切到字元中間,導致 panic |
使用 s.get(..2) 取得 Option<&str>,或先轉成 char 迭代 |
| 忘記釋放大字串 | 長時間持有巨大的 String 會佔用記憶體 |
使用 drop(s) 立即釋放,或在作用域結束時自動釋放 |
最佳實踐
- API 設計:公開函式盡量接受
&str,除非真的需要所有權。 - 最小化所有權搬移:使用
&my_string、&mut my_string進行借用,只有在需要產生新字串時才to_string()。 - 使用
Cow<'a, str>:當函式需要同時支援借用與擁有時,可返回Cow,讓呼叫端決定是否複製。 - 避免頻繁
String::push_str:若要大量拼接,先使用Vec<String>或String::with_capacity預先分配容量。
// 預先分配容量的範例
let mut big = String::with_capacity(1024); // 預留 1KB 空間
for i in 0..100 {
big.push_str(&format!("第 {} 行\n", i));
}
println!("{}", big);
實際應用場景
| 場景 | 建議使用 | 為什麼 |
|---|---|---|
命令列參數解析 (std::env::args) |
&str(借用) |
參數本身是 String,但大多只需讀取,不必搬移所有權 |
日誌訊息組合 (log::info!) |
format! 產生 String 再傳遞 |
format! 不消耗原始字串,且可一次完成多段文字拼接 |
| Web 伺服器回傳 JSON | String(擁有) |
回傳給框架的資料往往需要擁有所有權,才能在非同步環境中安全使用 |
| 文字搜尋與切割 | &str(slice) |
只需要讀取資料,不改變,使用 slice 可避免不必要的複製 |
| 編輯器或 IDE 中的文字緩衝區 | String(可變) |
使用者會不斷插入、刪除文字,需要可變的容器 |
總結
&str是 不可變的字串 slice,只提供對 UTF‑8 位元組的只讀視圖,適合 只讀、高效 的情境。String是 擁有所有權且可變 的容器,允許在執行時動態調整長度,適合需要 修改、擁有或跨執行緒傳遞 的情況。- 了解兩者的記憶體模型與所有權規則,能讓你在設計 API、寫效能關鍵程式碼時作出正確的選擇。
- 常見的陷阱(索引、未標記
mut、過度clone)只要遵循 借用與所有權的原則,配合as_str()、to_string()、format!等工具,就能寫出安全且高效的 Rust 程式。
掌握了 String 與 &str 的差異與最佳使用方式,你就能在 Rust 的文字處理上游刃有餘,為後續的更進階主題(如正則表達式、Unicode 正規化、異步 I/O)打下堅實的基礎。祝你寫程式快樂、寫程式安全!