本文 AI 產出,尚未審核

Rust 課程:集合型別 – 向量(Vectors)


簡介

在日常開發中,集合型別是儲存與處理多筆資料的基礎工具。Rust 提供了多種集合,最常使用的就是 向量(Vector)。向量是一種 可變長度連續記憶體 的陣列,它結合了陣列的索引存取速度與 Vec<T> 的彈性,讓我們可以在執行期自由地新增、刪除或重新排列元素。

對於剛接觸 Rust 的學習者而言,掌握向量的使用方式不僅能讓程式碼更簡潔,也能深入了解所有權、借用與記憶體安全的核心概念;對於已有一定基礎的開發者,熟練向量的最佳實踐則是提升效能與可讀性的關鍵。

本篇文章將從 概念、實作、常見陷阱實務應用,一步步帶你完整掌握 Rust 向量的使用方法。


核心概念

1. 什麼是 Vec<T>

Vec<T> 是 Rust 標準函式庫 (std::vec::Vec) 提供的動態陣列,T 代表向量中元素的型別。它的特性包括:

特性 說明
動態長度 可在執行時透過 pushpopinsert 等方法調整大小
連續記憶體 元素在堆上連續排列,支援 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);
}

重點pushpopinsertremove 都會 改變向量的長度,因此必須使用 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() 內呼叫 pushremove 會產生 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,依需求選擇讀寫鎖。

最佳實踐小結

  1. 預留容量:對於已知大小的資料批次,使用 with_capacityreserve 提前配置記憶體。
  2. 安全存取:除非確定索引合法,否則使用 get 取得 Option,避免 panic。
  3. 迭代分離:迭代期間不要改變向量的長度;若必須改變,先收集變更再執行。
  4. 適當的所有權:只在需要搬移或產生新集合時使用 into_iter,其餘情況保持借用。
  5. 記憶體回收:使用完大型向量後,呼叫 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 建立向量,根據需求提前配置容量。
  • 透過 pushpopinsertremove 操作元素,注意 借用規則長度變化
  • 安全存取時優先使用 get,避免因索引越界而 panic。
  • 依需求選擇適當的迭代器 (iteriter_mutinto_iter) 以控制所有權與可變性。
  • 在大量資料寫入或跨執行緒共享時,善用容量管理與同步原語(Arc<Mutex<_>>)。

只要遵循上述 最佳實踐,向量就能成為你在 Rust 生態系中最可靠、最具效能的資料容器。祝你在 Rust 的旅程中,玩得開心、寫得安心!