本文 AI 產出,尚未審核

Rust 集合型別:所有權與借用深入探討


簡介

在 Rust 中,集合(Vec<T>HashMap<K, V>HashSet<T> 等)是日常開發不可或缺的資料結構。它們不僅提供彈性的儲存與查找功能,更是 所有權(ownership)借用(borrowing) 機制的典型應用場景。了解集合在所有權與借用上的行為,能讓你寫出 安全、效能佳且避免資料競爭 的程式碼。

本篇文章將從概念說明、實作範例、常見陷阱到最佳實踐,帶你一步步掌握在集合上正確使用所有權與借用的技巧,適合剛踏入 Rust 的新手,也能為已有基礎的開發者提供進一步的思考與最佳化方向。


核心概念

1. 集合的所有權轉移

大多數集合型別在建立時會 取得 內部元素的所有權,當集合本身被移動(move)或傳遞給其他變數時,整個集合的所有權也會一起轉移。

fn main() {
    // 建立一個 Vec,內含三個整數
    let numbers = vec![1, 2, 3];
    // `numbers` 的所有權被移動到 `take_vec`
    take_vec(numbers);
    // 此行若取消註解,編譯會錯誤:`numbers` 已被移動
    // println!("{:?}", numbers);
}

fn take_vec(v: Vec<i32>) {
    println!("收到的向量長度: {}", v.len());
    // `v` 在此函式結束時被釋放,內部的 i32 也會被 drop
}

重點

  • Vec<T>HashMap<K, V>HashSet<T> 都是 堆配置 的容器,內部的元素會隨著容器的所有權一起搬移或釋放。
  • 若要在函式中 僅借用 集合,必須使用參考(&)或可變參考(&mut)。

2. 不可變借用:只讀存取

使用不可變參考時,集合本身 不會被移動,且在借用期間無法 變更其內容。這讓多個讀者可以同時安全地存取集合。

fn main() {
    let fruits = vec!["apple", "banana", "cherry"];
    // 同時借用多個不可變參考是允許的
    let a = count_fruits(&fruits);
    let b = first_fruit(&fruits);
    println!("總數: {}, 第一項: {}", a, b);
}

fn count_fruits(v: &Vec<&str>) -> usize {
    v.len()
}

fn first_fruit(v: &Vec<&str>) -> &str {
    // 直接返回切片中的元素引用
    v[0]
}

技巧

  • 若只需要讀取集合的部分資料,盡量使用切片(slice) 而非整個 Vec 的參考,以減少不必要的抽象層級。

3. 可變借用:修改集合

可變參考允許在借用期間改變集合的內容,但 同一時間只能有一個可變借用,以防止資料競爭。

fn main() {
    let mut scores = vec![10, 20, 30];
    // 可變借用,允許修改向量
    add_score(&mut scores, 40);
    println!("更新後: {:?}", scores);
}

fn add_score(v: &mut Vec<i32>, new_score: i32) {
    v.push(new_score); // 改變集合的長度與內容
}

注意:如果同時持有不可變與可變借用,編譯器會直接報錯,這是 Rust 保證安全的核心機制。

4. 迭代時的借用行為

迭代(foriter()into_iter())會決定是否取得所有權或僅借用集合:

迭代方式 取得的所有權 典型用途
v.iter() 不可變借用 (&T) 只讀遍歷
v.iter_mut() 可變借用 (&mut T) 需要修改元素
v.into_iter() 取得所有權 (T) 消費集合、搬移元素
fn main() {
    let mut nums = vec![1, 2, 3];

    // 只讀遍歷
    for n in nums.iter() {
        println!("只讀: {}", n);
    }

    // 可變遍歷,將每個元素加倍
    for n in nums.iter_mut() {
        *n *= 2;
    }
    println!("加倍後: {:?}", nums);

    // 消費遍歷,將向量搬走
    for n in nums.into_iter() {
        println!("搬走: {}", n);
    }
    // 此時 `nums` 已無效,若再使用會編譯錯誤
}

5. 內部可變性(Interior Mutability)

在某些情況下,我們希望在不可變借用的同時仍能修改集合的內容,這時可以使用 RefCell<T>RwLock<T> 等提供 內部可變性 的類型。

use std::cell::RefCell;

fn main() {
    // 使用 RefCell 包裝 Vec,允許在不可變參考下修改
    let data = RefCell::new(vec![1, 2, 3]);

    // 多個不可變參考仍然可以存在
    let r1 = data.borrow(); // 只讀
    println!("第一個只讀: {:?}", *r1);

    // 取得可變借用,修改內容
    {
        let mut w = data.borrow_mut();
        w.push(4);
    }

    // 再次只讀
    let r2 = data.borrow();
    println!("修改後: {:?}", *r2);
}

提醒:內部可變性會在執行時檢查借用規則,若違規會在執行時 panic;因此應謹慎使用。


程式碼範例

以下提供 五個實用範例,展示在不同情境下如何正確處理集合的所有權與借用。

範例 1:傳遞集合的所有權給多個函式(使用 clone

fn main() {
    let data = vec![100, 200, 300];

    // 需要在兩個不同的函式中使用相同的資料
    // 使用 clone 產生擁有相同內容的副本
    process_a(data.clone());
    process_b(data);
}

// 只讀處理
fn process_a(v: Vec<i32>) {
    println!("process_a: {:?}", v);
}

// 消費處理
fn process_b(v: Vec<i32>) {
    println!("process_b: sum = {}", v.iter().sum::<i32>());
}

小技巧:若資料量大且不需要真正的所有權轉移,可改用 Arc<Vec<T>> 共享所有權。

範例 2:使用切片避免不必要的所有權搬移

fn main() {
    let words = vec!["rust", "go", "python"];
    // 只需要讀取前兩個元素
    let slice = &words[0..2];
    print_slice(slice);
}

fn print_slice(s: &[&str]) {
    for w in s {
        println!("word: {}", w);
    }
}

範例 3:在迭代中同時保留不可變與可變借用(使用 enumerate

fn main() {
    let mut nums = vec![5, 10, 15];
    // 同時取得索引與可變元素
    for (i, n) in nums.iter_mut().enumerate() {
        *n += i as i32; // 依索引加值
    }
    println!("結果: {:?}", nums);
}

範例 4:利用 HashMapentry API 進行條件插入(借用與所有權混合)

use std::collections::HashMap;

fn main() {
    let mut scores = HashMap::new();
    // 插入或更新玩家分數
    update_score(&mut scores, "Alice", 30);
    update_score(&mut scores, "Bob", 20);
    update_score(&mut scores, "Alice", 10); // 累加
    println!("{:?}", scores);
}

fn update_score(map: &mut HashMap<&str, i32>, name: &str, add: i32) {
    // `entry` 會返回一個 Entry,允許我們在不重新查找的情況下插入或修改
    *map.entry(name).or_insert(0) += add;
}

範例 5:使用 RefCell 讓不可變借用的函式內部修改集合

use std::cell::RefCell;

fn main() {
    let shared_vec = RefCell::new(vec![1, 2, 3]);

    // 多個不可變參考同時存在
    read_vec(&shared_vec);
    mutate_vec(&shared_vec);
    read_vec(&shared_vec);
}

fn read_vec(v: &RefCell<Vec<i32>>) {
    let borrow = v.borrow(); // 只讀
    println!("讀取: {:?}", *borrow);
}

fn mutate_vec(v: &RefCell<Vec<i32>>) {
    let mut borrow_mut = v.borrow_mut(); // 可變
    borrow_mut.push(4);
    println!("已修改: {:?}", *borrow_mut);
}

常見陷阱與最佳實踐

陷阱 說明 解決方案
集合被意外移動 在傳遞 Vec<T> 給函式時忘記使用參考,導致後續無法使用 使用 &&mut,或在需要所有權時明確 clone
同時持有可變與不可變借用 編譯錯誤 cannot borrow ... as mutable because it is also borrowed as immutable 確保借用的生命週期不重疊;可利用作用域 ({}) 限制借用範圍
迭代時使用 into_iter 卻仍想保留集合 into_iter 會消費集合,使其失效 若仍需保留集合,改用 iter()iter_mut()
for 迴圈內同時修改集合長度 直接在 for 迴圈中 push/remove 會導致 panic 或錯誤行為 使用 while let 搭配 pop,或先收集要修改的索引再執行
忘記 RefCell 的執行時借用檢查 在多執行緒環境下使用 RefCell 會導致 panic 多執行緒使用 RwLock/Mutex,單執行緒使用 RefCell

最佳實踐

  1. 盡可能使用不可變借用:只讀操作不需要可變參考,能提升編譯器的最佳化空間。
  2. 利用切片與 iter 系列:避免不必要的所有權搬移,提升效能與可讀性。
  3. 在需要共享所有權時使用 Rc / ArcRc 適用單執行緒,Arc 適用多執行緒。
  4. 適時使用 entry API:對 HashMap 進行條件插入或更新時,避免二次查找。
  5. 遵守借用規則的生命週期:利用程式區塊 ({}) 明確限制借用範圍,讓編譯器更易推斷。

實際應用場景

場景 為何需要注意所有權與借用 典型解法
Web 伺服器的請求快取 多個請求同時讀取快取資料,且偶爾需要更新快取 使用 Arc<RwLock<HashMap<Key, Value>>>,讀取時取得 RwLockReadGuard,寫入時取得 RwLockWriteGuard
資料分析的批次處理 大量資料放在 Vec<Record>,每個階段只讀或局部修改 Vec 包成 &[Record] 傳遞給分析函式;若需要分段寫入,使用 &mut [Record]chunks_mut
遊戲引擎的實體管理 實體集合需要頻繁增刪,同時多個系統(渲染、物理)讀取 使用 Vec<Entity> 搭配 EntityId 索引;刪除時使用「延遲刪除」或「swap_remove」避免迭代期間改變長度
命令列工具的參數解析 解析後的參數存於 HashMap<String, String>,需要在多個子模組共享 HashMap 包成 Arc<HashMap<...>>,子模組只讀;若需要動態變更,改用 Arc<RwLock<...>>
嵌入式系統的緩衝區 固定大小的緩衝區 Vec<u8> 需要在 ISR(中斷服務例程)與主程式間共享 使用 static mut 搭配 unsafe(需極度小心)或 Mutex<RefCell<Vec<u8>>>(在單執行緒環境)

總結

集合型別是 Rust 程式設計中最常見的資料結構,而 所有權與借用 正是保證這些集合在編譯期即安全的核心機制。透過本文的概念說明與實作範例,你應該已能:

  • 判斷何時需要 轉移所有權、何時使用 不可變 / 可變借用
  • 正確選擇 迭代方式iteriter_mutinto_iter)以符合需求。
  • 避免常見的所有權搬移與借用衝突問題,並採用 最佳實踐(切片、entryRc/ArcRefCell)提升程式碼的可讀性與效能。

在實務開發中,掌握這些技巧不僅能寫出 安全、效能佳 的程式,也能更自信地面對多執行緒、資料共享與大型系統的挑戰。祝你在 Rust 的旅程中,玩得開心、寫得順手!