iT邦幫忙

2025 iThome 鐵人賽

DAY 13
0
Rust

Rust 逼我成為更好的工程師:從 Borrow Checker 看軟體設計系列 第 13

(Day13) Rust 零拷貝:切片 (Slice) 與 字串切片 (&str)

  • 分享至 

  • xImage
  •  

https://ithelp.ithome.com.tw/upload/images/20250917/20124462KA2M7PfuNm.png

Rust 逼我成為更好的工程師 零拷貝:切片(Slice)與字串切片(&str)

在 Rust 的世界裡,集合 (Vec, HashMap) 定義了資料的「擁有權邊界」。
這一篇把邊界再往內切一刀:如何將「一段資料」從「整份資料」中分離出來,並以零拷貝 (zero-copy) 的方式安全共享。

其精髓只有一句話:將資料的視圖 (View) 從容器 (Container) 中抽離,只留下語意與約束。

切片(Slice):代表「一段資料」的視圖型別

&[T]&str 是一種不擁有資料的參考型別

它們的主要語意是作為一個指向某段連續記憶體的 「借用視圖」
它們本身不負責記憶體的生與死,只提供一個有範圍、有約束的存取窗口。

這個設計將「我擁有這整份資料」的權力,與「我只想讀取其中一段」的需求徹底分離,讓資料共享變得幾乎沒有成本,且沒有意外的副作用。

切片是將「資料的一部分」提升為一等公民,讓你傳遞視圖,而非被迫複製資料。

fn main() {
    // 建立擁有者 (Vec<T>),它在記憶體中配置一塊連續的資料。
    let data: Vec<i32> = vec![10, 20, 30];

    // 從 'data' 建立一個切片視圖 (&[T])。
   // 包含索引 1,但不包含索引 3。也就是索引 1 和 2。
    let view: &[i32] = &data[1..3];
}

https://ithelp.ithome.com.tw/upload/images/20250927/20124462XiAdQYtQG0.png

切片(Slice) 作為「責任邊界」:更精細的權限管理

我們可以將資料互動分為三個層次,每一層都是一道更嚴格的責任邊界:

  1. 擁有者 (Owner) - String / Vec<T>:擁有資料,承擔配置與釋放記憶體的全部責任。

  2. 完整借用 (Borrow) - &String / &Vec<T>:暫時借用整個容器的讀取權,責任仍在擁有者身上。

  3. 視圖 (View) - &str / &[T]:只借用一段範圍的存取權,形成比容器更窄、更通用的責任區段。

最小權限原則

如果一個函式只需要讀取資料,就給它一個不可變的視圖 (&[T])。
如果它只需要讀取一個字串,就給它 &str
不要交出整個容器的所有權或可變性。

https://ithelp.ithome.com.tw/upload/images/20250927/20124462JNL42CtdFf.png

  • 擁有者 (Owner) 是權力的頂層,它控制著資料的生殺大權。
  • 它可以出借完整的借用 (Borrow),或是更精細的視圖 (View)
  • 視圖 (View) 是最基礎、最通用的層級,代表了最小權限原則。

String&str 的彈性

// 讀取視圖,回傳一個新的、擁有所有權的 String
fn shout(s: &str) -> String {
    s.to_uppercase()
}

fn main() {
    let owned_string = String::from("hello");
    let string_literal = "world";
    let part_of_string = &owned_string[0..2]; // "he"
    
	// 同一個函式,無縫接軌三種不同來源的字串資料
    let a = shout(&owned_string);
    let b = shout(string_literal);
    let c = shout(part_of_string);
}

https://ithelp.ithome.com.tw/upload/images/20250927/201244622IhYIPHVKn.png

資料互動的三種關係:讀、改、搬

  • 讀 (&[T] / &str):傳遞一個只讀視圖。這是最常見、最安全、零成本的操作。

  • 改 (&mut [T]):傳遞一個唯一的可變視圖。允許原地修改,但 Rust 的借用規則會保證在同一時間只有這一個可變視圖存在,將副作用完美地限制在一個範圍內。

  • 搬 (Vec<T> / String):轉移擁有權。當你需要為資料找到新的擁有者時使用。這是一個清晰的邊界劃分。

Vec 與 &[]:讀 / 改 / 拆視圖

let mut data = vec![10, 20, 30, 40, 50];

// 讀:任何需要 &[i32] 的 API 都能接受
fn sum(xs: &[i32]) -> i32 { xs.iter().sum() }
let total = sum(&data);

// 改:以 &mut [i32] 原地處理
fn scale(xs: &mut [i32], k: i32) { for x in xs { *x *= k; } }
scale(&mut data[1..4], 2);  // 只改中間那段

// 視圖:子切片零拷貝
let mid: &[i32] = &data[1..4];

生命週期的直覺:視圖活不過母體

切片只是一把「尺」,它量測的是其「母體」資料的一段長度。

如果母體資料消失了,這把尺的度量就失去了意義。

因此,Rust 編譯器強制規定:&[T]&str 的生命週期,永遠不能超過其來源資料的生命週期。

重點原則

  1. 切片不擁有資料,其創建永遠是零拷貝的。

  2. 切片的有效性由來源資料的生命週期決定,由編譯器保證安全。

  3. 切片讓 API 從「綁定具體容器」升級為「面向抽象資料視圖」。

以型別,取代執行期的 if/else

Rust 鼓勵我們將這些檢查移到編譯期。

  • 你不需要在函式內到處檢查「是不是字串常值?」、「是不是 String?」——簽名改成 &str 就好。
  • 你不需要在函式內到處檢查「會不會越界?」——切片建立時就保證邊界,之後型別只代表有效區段。
  • 你不需要在函式內到處檢查「會不會被別人同時修改?」——&mut [T] 的唯一借用保證了排他性。

結論:不只是 API,更是設計語言

Rust 的切片 (Slice) 機制不只是一個好用的工具,它是一種設計哲學的體現:

  1. 以視圖收斂 API 入口:用 &str&[T] 接受所有相容的資料來源,提升通用性。

  2. 以型別保障共享安全:讓編譯器代替你在執行期擔心生命週期和資料競爭。

  3. 以權限約束副作用:用 &mut [T] 將可變操作限制在最小的必要範圍內。

  4. 讓成本變得透明:當你需要新擁有者時,再透過 String::new().to_vec() 等方法明確地承擔記憶體分配的成本。

這套系統的主要思考是:不要輕易複製記憶體,而是給出一個帶有長度和生命週期約束的指標,然後讓強大的編譯器確保你不會搞砸。

相關連結與參考資源


上一篇
(Day12) Rust 集合 (Collection) 中的所有權:Vec、HashMap
系列文
Rust 逼我成為更好的工程師:從 Borrow Checker 看軟體設計13
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言