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. 迭代時的借用行為
迭代(for、iter()、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:利用 HashMap 的 entry 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 |
最佳實踐
- 盡可能使用不可變借用:只讀操作不需要可變參考,能提升編譯器的最佳化空間。
- 利用切片與
iter系列:避免不必要的所有權搬移,提升效能與可讀性。 - 在需要共享所有權時使用
Rc/Arc:Rc適用單執行緒,Arc適用多執行緒。 - 適時使用
entryAPI:對HashMap進行條件插入或更新時,避免二次查找。 - 遵守借用規則的生命週期:利用程式區塊 (
{}) 明確限制借用範圍,讓編譯器更易推斷。
實際應用場景
| 場景 | 為何需要注意所有權與借用 | 典型解法 |
|---|---|---|
| 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 程式設計中最常見的資料結構,而 所有權與借用 正是保證這些集合在編譯期即安全的核心機制。透過本文的概念說明與實作範例,你應該已能:
- 判斷何時需要 轉移所有權、何時使用 不可變 / 可變借用。
- 正確選擇 迭代方式(
iter、iter_mut、into_iter)以符合需求。 - 避免常見的所有權搬移與借用衝突問題,並採用 最佳實踐(切片、
entry、Rc/Arc、RefCell)提升程式碼的可讀性與效能。
在實務開發中,掌握這些技巧不僅能寫出 安全、效能佳 的程式,也能更自信地面對多執行緒、資料共享與大型系統的挑戰。祝你在 Rust 的旅程中,玩得開心、寫得順手!