iT邦幫忙

1

二、三天學一點點 Rust:來! Slices(25)

  • 分享至 

  • xImage
  •  

📚 字串切片(String Slice)

Rust 提供強大而安全的字串切片功能,可以有效地從字串中提取部分內容,不需複製也不會轉移所有權。這邊依照四個主題說明:從 String 創建切片、字串字面值與切片、len() 與 UTF-8 字元長度、語法簡寫技巧(Syntactic shortcuts)。


✂️ 從 String 創建字串切片

fn main() {
    let full_name = String::from("Chen Roger"); // 創建一個 String
    let slices_name = &full_name[0..4];          // 從 String 創建一個字串切片
    println!("{}", slices_name);                 // 印出 "Chen"
}
  • let slices_name = &full_name[0..4]
    • & (借用操作符):我們使用借用操作符 & 來表示我們要創建一個引用,而不是取得數據的所有權。字串切片本質上就是一個引用。
    • 我們要從 full_name 這個 String 中獲取一部分。
    • [0..4] (範圍語法): 這指定了我們想要切片的部分。重要的是,這裡的索引是 基於位元組 (byte) 的索引,而不是基於字元 (character) 的索引。
      • 字節計算細節: 在字串 "Chen Roger" 中,如果我們假設是 ASCII 或簡單的 UTF-8 字元:
        • 'C' 佔用 1 個位元組,在索引 0。
        • 'h' 佔用 1 個位元組,在索引 1。
        • 'e' 佔用 1 個位元組,在索引 2。
        • 'n' 佔用 1 個位元組,在索引 3。
        • 所以,[0..4] 表示從索引 0 開始,直到索引 4 (但不包含索引 4) 的所有位元組。這恰好是 "Chen" 這四個字元所佔用的位元組。
        • 為何是字節索引: Rust 的 String 是 UTF-8 編碼的。在 UTF-8 中,一個字元可能佔用 1 到 4 個位元組。使用字節索引可以精確控制記憶體片段,但這也意味著如果切片邊界不在一個 UTF-8 字元的邊界上,程式可能會 panic (例如,你不能將一個佔 3 字節的字元從中間切開)。對於 "Chen" 這樣只包含單字節字元的字串,字元索引恰好等於字節索引。
  • slices_name 的類型是 &str (一個字串切片引用)。它指向 full_name 所擁有的堆積記憶體中 "Chen" 這部分數據。而slices_name 並不擁有這些數據,它只是借用了它們。

🔁 字串切片與字串字面值(String Literals)

這個程式主要在於我們現在是從一個「字串字面值」(String Literal) 而不是一個 String 物件來創建切片。

fn main() {
    let first_name = {
        let name = "Chen Roger";     // name 是字串字面值,型別為 &str
        &name[0..4]                   // 從 &str 建立另一個 &str 切片
    }; // 雖然 name 在內部作用域,但 first_name 仍然有效,因為是借用靜態字串

    println!("{}", first_name);       // 印出 "Chen"
}
  • let name = "Chen Roger";
    • 這裡,"Chen Roger" 是一個字串字面值。在 Rust 中,字串字面值的類型是 &'static str
    • &str 表示它是一個字串切片。
    • 'static (靜態生命週期) 表示這個字串數據是直接嵌入到程式的二進制文件中的,並且在程式的整個運行期間都有效。所以,變數 name 是一個指向這個靜態儲存區域的引用。
    • 即使 name 變數離開作用域,因為它是靜態資料,所以 first_name 仍然有效。
  • let first_name = { ... &name[0..4] };
    • 這裡我們使用了一個區塊 ({...}) 來創建一個內部作用域。
    • 在內部作用域中,name 被賦予了 "Chen Roger" 這個字串字面值的引用。
    • &name[0..4]
      • 我們再次使用範圍 [0..4] 來從 name 所引用的字串數據中提取一部分。
      • 由於 name 本身已經是一個 &'static str (一個引用),對它進行切片操作 name[0..4] 會得到一個概念上的 str (雖然 str 本身不能直接作為變數類型,因為它的大小不固定)。然後,我們再對這個結果取引用 &,所以 &name[0..4] 的結果仍然是一個 &str
      • 因為 name 指向的是具有 'static 生命週期的數據,所以 first_name 得到的切片也將具有 'static 生命週期,即 first_name 的類型也是 &'static str。它指向靜態記憶體中 "Chen" 這部分。

✅ 總結這兩段程式的依賴關係

  • 在第一個例子中,slices_name: &str 依賴於 full_name: String 的存在。如果 full_name 被銷毀,slices_name 就會變成一個懸垂引用(Rust 的借用檢查器會阻止這種情況發生)。
  • 在第二個例子中,first_name: &'static str 是在一個內部作用域中根據 name: &'static str 創建的。當內部作用域結束時,name 這個 變數綁定 本身確實消失了。然而,name 所指向的數據 "Chen Roger" 是一個字串字面值,它存在於程式的靜態記憶體區域,擁有 'static 生命週期,意味著它在整個程式運行期間都有效。
  • 因此,first_name 引用的是這塊靜態記憶體中的 "Chen" 部分。儘管創建它的 name 變數 不再存在於 first_name 的作用域中,但 first_name 所引用的 數據 仍然有效。所以,first_name 依賴的是那塊靜態儲存的字串數據,而不是 name 這個局部變數的持續存在。

📏 字串長度與 UTF-8 編碼的注意事項(len)

fn main() {
    let moji = "🤣";                    // 單一 emoji
    println!("{}", moji.len());         // 輸出 4(UTF-8 編碼為 4 bytes)

    // 下行程式會造成 panic,因為索引切到不完整的 UTF-8 字元
    // let moji_slice = &moji[0..3];
    // println!("{}", moji_slice);
}
  • String&str.len() 回傳的是位元組長度,而非字元數量。
  • Rust 的字串使用 UTF-8 編碼,有些字元(如 emoji)是多位元組(byte)的。

✅ 延伸補充範例

fn main() {
    let hello = "hello";
    println!("長度為:{}", hello.len()); // 輸出 5,因為每個字母是 1 byte

    let zh = "哈囉";
    println!("長度為:{}", zh.len());   // 輸出 6,每個中文字是 3 bytes
}

✨ 語法簡寫技巧(Syntactic Shortcuts)

fn main() {
    let action_hero = String::from("Arnold Schwarzenegger");

    let first_name = &action_hero[..6];
    println!("His first name is {first_name}.");

    let last_name = &action_hero[7..];
    println!("His last name is {last_name}.");

    let full_name = &action_hero[..];
    println!("His full name is {full_name}.");
}
  • [..6] 代表從開頭到第 6 個 byte。
  • [7..] 代表從第 7 個 byte 到字串結尾。
  • [..] 表示整個字串的切片。

&action_hero[..]&action_hero 的差異

&action_hero[..]中,它的意思是選取整個字串,從開頭到末尾。因為是個&str類型,所以創建了一個指向 action_hero 所擁有的實際字串數據(儲存在堆積上的字元序列)的切片。這個切片覆蓋了 action_hero 的全部內容。&str 是一個「胖指針」(fat pointer),它包含兩個部分:一個指向字串數據起始位置的 指針 (pointer)、切片的 長度 (length),以字節為單位。
因此,當你想要一個不可變的、對字串內容的直接引用時,通常使用 &str。很多處理字串數據的函式都接受 &str 作為參數,因為它更通用(既可以引用 String 的一部分或全部,也可以引用字串字面值)。

&action_hero,其型別是&String (一個對 String 結構體的引用),而String 結構體是一個管理者,它內部包含了指向堆上字串數據的指針,以及該數據的長度和容量 (capacity)。內部結構 (被引用的 String 結構體)中,包含:一個指向堆積上實際字串數據的 指針 (pointer)、字串的目前 長度 (length)、堆上緩衝區的總 容量 (capacity)。當你需要引用整個 String 物件,並且可能需要存取 String 特有的方法或其長度/容量等元數據時,你會使用 &String

✅主要差異點總結:

本質不同:

  • &action_hero[..] (&str) 是一個對 字串數據本身 的直接視圖/切片。
  • &action_hero (&String) 是一個對 管理字串數據的結構體 的引用。

資訊含量:

  • &str 直接告訴你數據在哪裡以及有多長。
  • &String 告訴你 String 這個「容器」在哪裡,你可以透過這個容器找到數據以及容器的容量等資訊。

🧩 String Slice 作為函式參數(&str as Function Parameters)

這節的主軸是以「字串切片作為函式參數」(String Slice as Function Parameters),並說明 &String 和 &str 之間的轉換關係。在 Rust 中,當你編寫一個需要讀取字串數據但不獲取其所有權的函式時,通常最符合習慣且最具彈性的做法是讓參數型別為 &str(字串切片)。

fn main() {
    let action_hero = String::from("Arnold Schwarzenegger");
    do_hero_stuff(&action_hero); // 傳入 &String(自動轉為 &str)

    let other_action_hero = "Sylvester Stallone"; // 字串字面值為 &str
    do_hero_stuff(other_action_hero);
}

fn do_hero_stuff(the_hero: &str) {
    println!("{} saves the day.", the_hero);
}

✅ 程式碼說明

  1. 傳遞 &String&str 參數 ( &String -> &str )
  • do_hero_stuff(&action_hero);,在這行程式碼中,&action_hero創建了一個對String物件action_hero的引用,因此它的型別是&String。但是,在 do_hero_stuff 函式期望的參數型別是 &str。而這個函式能成功轉換是因為 Rust 的一個重要特性叫做 Deref Coercion (解引用轉換)。因為String 型別實作了 Deref <Target = str> Trait。這個 Trait 允許 String 物件(或者更準確地說,是對 String 的引用)在需要時被自動「解引用」成 str 型別的引用(即 &str)。所以,當你將 &String 傳遞給一個 &str 的函式時,編譯器會自動進行轉換,使得 &action_hero 就像一個指向其內部字串數據的 &str 切片一樣被使用。這實際上等同於傳遞了 &action_hero[..]action_hero.as_str()
  1. 傳遞 &str (字串字面值) 給 &str 參數
  • do_hero_stuff(other_action_hero);,在這行程式碼中,other_action_hero本身即為一個&str,它與 do_hero_stuff 函式參數 the_hero: &str 的型別直接匹配。因此,這裡不需要任何特殊的轉換,可以直接傳遞。

❌ 反過來 &str -> &String 呢?

  • &str 不是 heap 所有者,不能轉為 &String
  • 直接的 &str -> &String 隱式轉換通常不存在,因為它們代表了不同的概念(一個是數據視圖,一個是對數據容器結構的引用)。

📘 總結對照表

類型來源 型別 可否傳給 &str 函式? 備註
字串字面值 &'static str ✅ 是 最常見,也最安全
String 變數 String ✅ 是(需加 & &String 會自動轉為 &str
&String &String ✅ 是 自動解參照為 &str
&str &str ✅ 是 完全相容
&strString ❌ 不能直接轉 可用 .to_string().into()

透過使用 &str 作為函式參數,可以讓你的 API 更通用,同時支援 String 與字串字面值,大幅增加彈性與效率。


圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言