本文 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 編碼的位元組。
  • 可變長度:可以使用 pushpush_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. 轉換方法

從 → 到 方法 會不會搬移所有權?
&strString 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) 立即釋放,或在作用域結束時自動釋放

最佳實踐

  1. API 設計:公開函式盡量接受 &str,除非真的需要所有權。
  2. 最小化所有權搬移:使用 &my_string&mut my_string 進行借用,只有在需要產生新字串時才 to_string()
  3. 使用 Cow<'a, str>:當函式需要同時支援借用與擁有時,可返回 Cow,讓呼叫端決定是否複製。
  4. 避免頻繁 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)打下堅實的基礎。祝你寫程式快樂、寫程式安全!