本文 AI 產出,尚未審核

Rust 課程 – 所有權與借用

主題:切片(Slices)


簡介

在 Rust 中,切片(slice)是對集合(如陣列、Vec<T>、字串)的一段連續記憶體區間的「只讀」或「可變」視圖。它讓我們在不取得所有權的情況下,安全且高效地存取資料子集合。
對於剛踏入 Rust 的開發者而言,切片是連結
所有權
借用安全記憶體存取的核心橋樑;掌握它不僅能寫出更具表現力的程式碼,也能避免常見的記憶體錯誤。

本篇文章將從概念說明、實作範例、常見陷阱到實務應用,完整呈現切片在 Rust 生態系中的角色,讓 初學者 能快速上手,中級開發者 能進一步優化程式設計。


核心概念

1. 什麼是 Slice

  • 定義&[T](不可變切片)或 &mut [T](可變切片)是對一段連續 T 型別資料的引用。
  • 特性
    • 不擁有資料:切片本身不會釋放或搬移底層資料。
    • 長度已知:切片在編譯時即知道自己的長度(len()),因此在執行時不會產生額外的邊界檢查成本。
    • 安全:所有的存取都受到 Rust 編譯器的借用檢查(borrow checker)保護,避免了野指標與緩衝區溢位。

2. 建立 Slice

來源 建立方式 範例
陣列 &arr[start..end] let s = &arr[2..5];
Vec<T> 同上,或 vec.as_slice() let s = &v[..];
字串 (String / &str) &str[start..end](以位元組為單位) let sub = &s[0..4];
可變切片 &mut arr[start..end] let s = &mut arr[1..3];

注意:字串切片的索引必須落在 UTF-8 字元邊界上,否則編譯會錯誤。

3. 為什麼使用 Slice

  • 避免不必要的複製:直接借用底層資料,省去 clone() 的成本。
  • 函式介面統一:接受 &[T] 的函式可以同時處理陣列、Vec<T>、甚至是其他切片,提升 API 的彈性。
  • 安全的子集合操作:切片自動執行邊界檢查,保證不會讀寫超出範圍的記憶體。

程式碼範例

下面的範例以 Rust 為語言(使用 rust 標記),每段都附上說明註解,展示切片的常見操作與技巧。

範例 1:從陣列取得不可變切片

fn main() {
    // 一個固定長度的陣列
    let numbers: [i32; 6] = [10, 20, 30, 40, 50, 60];

    // 取得索引 1~4(不含 4)的切片
    let slice: &[i32] = &numbers[1..4];
    // slice 的內容是 [20, 30, 40]

    // 使用迭代印出每個元素
    for (i, val) in slice.iter().enumerate() {
        println!("slice[{}] = {}", i, val);
    }
}

重點&numbers[1..4] 只借用了陣列的一部分,numbers 本身仍然保有所有權。

範例 2:可變切片修改資料

fn main() {
    let mut data = vec![5, 10, 15, 20, 25];

    // 取得可變切片,修改中間兩個元素
    {
        let slice: &mut [i32] = &mut data[1..3];
        slice[0] = 100; // 把原本的 10 改成 100
        slice[1] = 200; // 把原本的 15 改成 200
    } // slice 在此離開作用域,借用結束

    // 檢查結果
    println!("data = {:?}", data); // [5, 100, 200, 20, 25]
}

技巧:將可變切片限制在區塊 ({}) 內,使借用在使用完畢後立即結束,避免與其他不可變借用衝突。

範例 3:函式接受任意切片

// 計算任意切片的平均值
fn average(slice: &[f64]) -> f64 {
    let sum: f64 = slice.iter().sum();
    sum / slice.len() as f64
}

fn main() {
    let a = [1.0, 2.0, 3.0];
    let v = vec![4.0, 5.0, 6.0, 7.0];

    println!("a 的平均值 = {}", average(&a));
    println!("v 的平均值 = {}", average(&v));
}

優點average 只需要 &[f64],因此既能接受陣列、Vec,也能接受其他切片,API 具備高度彈性

範例 4:字串切片與 UTF-8 安全

fn main() {
    let text = String::from("Rust 🦀 語言");

    // 取得前四個位元組(剛好是 "Rust")
    let hello = &text[0..4];
    println!("{}", hello); // Rust

    // 嘗試切到表情符號的中間會編譯錯誤
    // let broken = &text[5..7]; // error: byte index 5 is not a char boundary
}

提醒:字串切片的索引必須落在 字元邊界(char boundary),否則編譯器會直接報錯,避免了 UTF-8 破碎的問題。

範例 5:使用 split_at 取得兩段切片

fn main() {
    let nums = [1, 2, 3, 4, 5, 6];

    // 同時得到左半部與右半部的切片
    let (left, right) = nums.split_at(3);
    // left = &[1, 2, 3],right = &[4, 5, 6]

    println!("左半部: {:?}", left);
    println!("右半部: {:?}", right);
}

實務split_at 常用於 二分搜尋資料切分 等演算法,讓程式碼更具可讀性。


常見陷阱與最佳實踐

陷阱 說明 解決方式
切片長度與索引不一致 使用 slice[i] 時若 i >= slice.len(),會在執行時 panic。 使用 get(i) 回傳 Option<&T>,或在迭代時使用 foriter()
字串切片的 UTF-8 邊界 直接以位元組索引切割可能破壞多位元組字元。 使用 char_indices()unicode_segmentation crate,或只在已知邊界上切割。
可變切片與不可變借用衝突 同時存在 &mut [T]&[T] 會觸發 borrow checker 錯誤。 把可變切片的使用範圍限制在最小區塊,或在需要同時讀寫時使用 split_at_mut
忘記切片的生命週期 切片的生命週期受原始資料限制,若原始資料被釋放,切片會成為懸掛引用。 確保切片的使用範圍不超過原始資料的作用域,或使用 Arc<[T]>Rc<[T]> 共享所有權。
過度使用 clone() 取得切片 有時開發者會 vec.clone() 再切片,失去零拷貝的好處。 直接借用 &vec[..]&mut vec[..],除非真的需要所有權的副本。

最佳實踐

  1. 盡量使用不可變切片:除非需要修改,否則 &[T] 能提供更寬鬆的借用規則。
  2. 利用標準函式split_at, chunks, windows 等可直接產生切片迭代器,避免手動索引。
  3. 在 API 設計上以切片為參數:讓函式接受 impl AsRef<[T]>&[T],提升相容性。
  4. 使用 debug_assert! 檢查前置條件:在開發階段加入長度或邊界檢查,確保不會在 release 版 panic。

實際應用場景

場景 為何適合使用 Slice 範例
文字處理(例如:分詞、搜尋) 只需要讀取字串子區段,不想搬移資料 let word = &text[start..end];
圖形與音訊緩衝區 大量資料以連續記憶體儲存,切片允許零拷貝的子區段處理 let frame = &buffer[offset..offset+size];
演算法實作(二分搜尋、滑動視窗) slice[mid]slice.windows(k) 等直接提供安全的索引 binary_search(&arr, target);
網路封包解析 解析封包時只需要取出 header、payload 等部份 let header = &packet[..HEADER_LEN];
測試與模擬 測試函式只需要提供切片作為輸入,避免建立完整集合 assert_eq!(process(&data[..3]), expected);

總結

切片是 Rust 所有權與借用 系統中最常用、最實用的抽象之一。透過 &[T]&mut [T],我們可以在不取得所有權的前提下,安全且高效地操作資料子集合。掌握以下要點,即可在日常開發中發揮切片的威力:

  1. 建立切片:使用範圍語法 &data[start..end],或 as_slice()split_at 等輔助函式。
  2. 遵守借用規則:不可同時持有可變與不可變切片,必要時使用 split_at_mut 或縮小作用域。
  3. 注意 UTF-8 邊界:字串切片必須落在合法的字元邊界上。
  4. 利用標準庫工具chunks, windows, iter() 等讓切片操作更簡潔。
  5. 在 API 中以切片為介面:提升函式的彈性與可重用性。

只要熟練上述概念與實務技巧,您就能在 資料處理、演算法實作、系統程式 等各種情境下,寫出 安全、效能優秀且易於維護 的 Rust 程式碼。祝您在 Rust 的所有權與借用世界裡玩得開心,寫出更好的程式!