Rust 課程:集合型別 – 向量(Vectors)
簡介
在日常開發中,集合型別是儲存與處理多筆資料的基礎工具。Rust 提供了多種集合,最常使用的就是 向量(Vector)。向量是一種 可變長度、連續記憶體 的陣列,它結合了陣列的索引存取速度與 Vec<T> 的彈性,讓我們可以在執行期自由地新增、刪除或重新排列元素。
對於剛接觸 Rust 的學習者而言,掌握向量的使用方式不僅能讓程式碼更簡潔,也能深入了解所有權、借用與記憶體安全的核心概念;對於已有一定基礎的開發者,熟練向量的最佳實踐則是提升效能與可讀性的關鍵。
本篇文章將從 概念、實作、常見陷阱 到 實務應用,一步步帶你完整掌握 Rust 向量的使用方法。
核心概念
1. 什麼是 Vec<T>
Vec<T> 是 Rust 標準函式庫 (std::vec::Vec) 提供的動態陣列,T 代表向量中元素的型別。它的特性包括:
| 特性 | 說明 |
|---|---|
| 動態長度 | 可在執行時透過 push、pop、insert 等方法調整大小 |
| 連續記憶體 | 元素在堆上連續排列,支援 O(1) 的索引存取 |
| 所有權安全 | 依照所有權與借用規則,避免資料競爭與野指標 |
| 自動釋放 | 向量離開作用域時會自動呼叫 drop 釋放記憶體 |
備註:向量在底層使用 容量(capacity) 與 長度(len) 兩個概念。容量是已分配的記憶體大小,長度是實際已填入的元素數量。
push時若長度已等於容量,向量會自動重新配置(reallocate)更大的緩衝區。
2. 建立向量
// 建立一個空的向量,型別必須明確指出
let mut v: Vec<i32> = Vec::new();
// 使用宏 `vec!` 建立並初始化
let numbers = vec![1, 2, 3, 4, 5];
// 從迭代器產生向量
let from_iter: Vec<String> = (0..3).map(|i| format!("item-{}", i)).collect();
Vec::new()需要明確的型別,否則編譯器無法推斷。vec![]宏會根據提供的字面值自動推斷型別,最常用且語意最清晰。
3. 基本操作
| 方法 | 功能 | 範例 |
|---|---|---|
push |
在尾端加入元素 | v.push(10); |
pop |
移除最後一個元素,回傳 Option<T> |
let last = v.pop(); |
insert |
在指定索引插入元素 | v.insert(2, 99); |
remove |
移除指定索引的元素,回傳該元素 | let x = v.remove(0); |
len / is_empty |
取得長度或檢查是否為空 | v.len(); v.is_empty(); |
capacity |
取得目前分配的容量 | v.capacity(); |
reserve / shrink_to_fit |
手動調整容量 | v.reserve(10); v.shrink_to_fit(); |
程式碼範例 1:基本增刪
fn main() {
// 建立可變向量
let mut scores = vec![10, 20, 30];
println!("原始向量: {:?}", scores);
// push - 加到尾端
scores.push(40);
println!("push 後: {:?}", scores);
// pop - 移除最後一個
if let Some(last) = scores.pop() {
println!("pop 取得: {}", last);
}
println!("pop 後: {:?}", scores);
// insert - 在索引 1 插入 15
scores.insert(1, 15);
println!("insert 後: {:?}", scores);
// remove - 移除索引 0
let removed = scores.remove(0);
println!("remove 取得: {}", removed);
println!("最終向量: {:?}", scores);
}
重點:
push、pop、insert、remove都會 改變向量的長度,因此必須使用mut變數。
4. 索引與切片
向量支援 索引運算子 [] 以及 切片 &[T]。索引存取在編譯時不會檢查邊界,若超出範圍會直接 panic;若想安全地取得元素,請使用 get 方法。
let v = vec![10, 20, 30];
// 直接索引(可能 panic)
let third = v[2];
println!("第三個元素: {}", third);
// 安全取得(回傳 Option)
match v.get(5) {
Some(val) => println!("第六個元素: {}", val),
None => println!("索引超出範圍"),
}
切片則允許我們在不產生所有權轉移的情況下,借用 向量的一段連續區間:
let slice: &[i32] = &v[0..2]; // 包含索引 0、1
println!("切片內容: {:?}", slice);
5. 迭代與所有權
向量提供多種迭代器:
| 迭代器 | 取得方式 | 取得的型別 |
|---|---|---|
iter() |
&v.iter() |
&T(不可變借用) |
iter_mut() |
&mut v.iter_mut() |
&mut T(可變借用) |
into_iter() |
v.into_iter() |
T(取得所有權) |
程式碼範例 2:不同迭代方式的差異
fn main() {
let mut numbers = vec![1, 2, 3, 4];
// 不可變迭代:只能讀取
for n in numbers.iter() {
println!("不可變讀取: {}", n);
}
// 可變迭代:可以修改元素
for n in numbers.iter_mut() {
*n *= 2; // 每個元素乘以 2
}
println!("可變迭代後: {:?}", numbers);
// 取得所有權:消費向量
let owned: Vec<i32> = numbers.into_iter().map(|x| x + 1).collect();
println!("取得所有權後: {:?}", owned);
}
技巧:若只需要讀取,盡量使用
iter();若要修改,使用iter_mut();若要把向量「搬走」並產生新集合,使用into_iter()。
6. 記憶體與容量管理
向量的 容量 會隨著 push 自動擴增,擴增策略通常是 成長兩倍(具體實作會根據平台略有差異)。在大量資料寫入前,我們可以預先 預留容量,減少重新配置的次數:
let mut big_vec = Vec::with_capacity(1_000_000);
big_vec.reserve(500_000); // 再額外保留 50 萬
println!("容量: {}", big_vec.capacity()); // 150 萬
若向量在使用完畢後仍保有過大的容量,可呼叫 shrink_to_fit() 讓記憶體回收:
big_vec.shrink_to_fit();
println!("收縮後容量: {}", big_vec.capacity());
常見陷阱與最佳實踐
| 陷阱 | 說明 | 解決方式 |
|---|---|---|
| 索引越界 panic | 直接使用 v[i] 超出範圍會導致程式崩潰。 |
使用 v.get(i) 取得 Option<T>,或在開發階段加上 debug_assert!(i < v.len()); |
| 迭代時同時修改長度 | 在 for x in v.iter() 內呼叫 push、remove 會產生 borrow checker 錯誤。 |
先收集要插入/刪除的資訊,迭代結束後再統一修改;或使用 while let Some(x) = v.pop() 這類「消費」迭代。 |
| 不必要的所有權搬移 | v.into_iter() 會把向量消耗掉,若僅想讀取會造成不必要的搬移。 |
盡量使用 iter() 或 iter_mut(),只在真的需要所有權時才使用 into_iter()。 |
| 容量過大導致記憶體浪費 | 先前 reserve 太多,最後只用了少量元素。 |
使用 shrink_to_fit() 或在不需要大量容量時直接 Vec::new()。 |
跨執行緒共享時忘記使用 Arc<Mutex<Vec<T>>> |
向量本身不是 thread‑safe,直接傳遞會產生編譯錯誤或資料競爭。 | 包裝成 Arc<Mutex<Vec<T>>> 或 RwLock,依需求選擇讀寫鎖。 |
最佳實踐小結
- 預留容量:對於已知大小的資料批次,使用
with_capacity或reserve提前配置記憶體。 - 安全存取:除非確定索引合法,否則使用
get取得Option,避免 panic。 - 迭代分離:迭代期間不要改變向量的長度;若必須改變,先收集變更再執行。
- 適當的所有權:只在需要搬移或產生新集合時使用
into_iter,其餘情況保持借用。 - 記憶體回收:使用完大型向量後,呼叫
shrink_to_fit或讓向量自行離開作用域,以釋放不必要的記憶體。
實際應用場景
| 場景 | 為何選擇 Vec<T> |
範例程式碼 |
|---|---|---|
| 讀寫檔案的逐行緩衝 | 行數未知且需要隨時加入新行 | let mut lines = Vec::new(); while let Some(l) = read_line()? { lines.push(l); } |
| 即時資料收集(IoT) | 感測器資料持續累積,偶爾批次上傳 | sensor_buffer.push(value); if sensor_buffer.len() >= BATCH_SIZE { upload(&sensor_buffer); sensor_buffer.clear(); } |
| 圖形演算法中的頂點列表 | 頂點數量在運算過程中會變化 | let mut vertices: Vec<Point> = Vec::new(); // 動態加入新頂點 |
| 文字處理:詞彙統計 | 需要動態插入新詞,且頻繁遍歷 | let mut words: Vec<String> = Vec::new(); for w in text.split_whitespace() { if !words.contains(&w.to_string()) { words.push(w.to_string()); } } |
| 多執行緒工作隊列 | 需要在多個執行緒間共享可變集合 | let queue = Arc::new(Mutex::new(Vec::<Job>::new())); |
實務提醒:在需要頻繁在中間位置插入/刪除的情況下,
Vec<T>可能不是最佳選擇,此時可考慮LinkedList<T>或VecDeque<T>(雙端佇列)來降低搬移成本。
總結
向量(Vec<T>)是 Rust 中最常用、最靈活的集合型別。它結合了 安全的所有權模型、高效的連續記憶體存取,以及 豐富的 API,讓開發者能在不犧牲安全性的前提下,輕鬆處理動態大小的資料。掌握以下要點,即可在實務開發中發揮向量的最大威力:
- 使用
vec![]或Vec::with_capacity建立向量,根據需求提前配置容量。 - 透過
push、pop、insert、remove操作元素,注意 借用規則 與 長度變化。 - 安全存取時優先使用
get,避免因索引越界而 panic。 - 依需求選擇適當的迭代器 (
iter、iter_mut、into_iter) 以控制所有權與可變性。 - 在大量資料寫入或跨執行緒共享時,善用容量管理與同步原語(
Arc<Mutex<_>>)。
只要遵循上述 最佳實踐,向量就能成為你在 Rust 生態系中最可靠、最具效能的資料容器。祝你在 Rust 的旅程中,玩得開心、寫得安心!