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>,或在迭代時使用 for、iter()。 |
| 字串切片的 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[..],除非真的需要所有權的副本。 |
最佳實踐
- 盡量使用不可變切片:除非需要修改,否則
&[T]能提供更寬鬆的借用規則。 - 利用標準函式:
split_at,chunks,windows等可直接產生切片迭代器,避免手動索引。 - 在 API 設計上以切片為參數:讓函式接受
impl AsRef<[T]>或&[T],提升相容性。 - 使用
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],我們可以在不取得所有權的前提下,安全且高效地操作資料子集合。掌握以下要點,即可在日常開發中發揮切片的威力:
- 建立切片:使用範圍語法
&data[start..end],或as_slice()、split_at等輔助函式。 - 遵守借用規則:不可同時持有可變與不可變切片,必要時使用
split_at_mut或縮小作用域。 - 注意 UTF-8 邊界:字串切片必須落在合法的字元邊界上。
- 利用標準庫工具:
chunks,windows,iter()等讓切片操作更簡潔。 - 在 API 中以切片為介面:提升函式的彈性與可重用性。
只要熟練上述概念與實務技巧,您就能在 資料處理、演算法實作、系統程式 等各種情境下,寫出 安全、效能優秀且易於維護 的 Rust 程式碼。祝您在 Rust 的所有權與借用世界裡玩得開心,寫出更好的程式!