iT邦幫忙

2025 iThome 鐵人賽

DAY 12
0
Rust

Rust 逼我成為更好的工程師:從 Borrow Checker 看軟體設計系列 第 12

(Day12) Rust 集合 (Collection) 中的所有權:Vec、HashMap

  • 分享至 

  • xImage
  •  

https://ithelp.ithome.com.tw/upload/images/20250917/20124462KA2M7PfuNm.png

Rust 逼我成為更好的工程師:集合 (Collection) 中的所有權:Vec、HashMap

這次要聊的是最常見的集合型別——VecHashMap

在遇見 Rust 之前,我一直覺得集合(Collection),不就是個資料而已嗎?
把東西丟進去、拿出來、算算有幾個,很直覺。
但 Rust 卻給了我一記當頭棒喝,它告訴我:集合不只是容器,它更是一道「責任邊界」

集合不只是容器,集合是「責任邊界」

「責任邊界」是什麼意思?
在 Rust 的世界裡,當你把資料放進一個集合,你其實是在做一個重要的決定:

  • 誰擁有這些資料? 是這個集合嗎?

  • 誰該負責在最後把它們清理乾淨?

  • 誰只能在旁邊看看(讀取)?

  • 誰才有權力動手修改?
    https://ithelp.ithome.com.tw/upload/images/20250926/201244628Qf5uGqOGY.png

最關鍵的是,這份「責任歸屬」不是靠團隊默契或個人自律,而是 由編譯器強制執行的一份合約
這就是它為何如此可靠。

簡單一句話:Rust 的集合讓「資料的命運」變得可預測、可檢查,甚至在編譯時期就能被證明。

Vec:把「資料流」拆成三種關係,其他全是特例

我們對集合最常見的操作是什麼?不是新增或刪除,而是在一段程式邏輯中,「讀取它、修改它、或把它帶走」。

Rust 的 Vec 把這三種互動,直接升級成了三種不同的「關係」:

  1. 讀取(借用 Borrow):我只是想看看,不會動手,資料本身還在原地。

  2. 修改(可變借用 Mutable Borrow):我要動手了,而且為了安全,此刻只有我能動它。

  3. 搬走(移動所有權 Move):這東西現在歸我了,原主人不能再碰。

https://ithelp.ithome.com.tw/upload/images/20250926/20124462VqQhhfCPU1.png

這不是 API 表面技巧,而是把「資料的三種命運」變成了三個不同的型別關係。
結果是程式碼變得超級直覺:

  • 讀取時不可能意外改到資料。

  • 修改時不用擔心其他地方同時也在改。

  • 把資料搬走後,編譯器會確保你不會再誤用舊的那個。

我們可能會覺得:「哇,這麼嚴格?」
沒錯,這正是重點。

Rust 選擇把複雜度擋在編譯期,換來一個乾淨、可預測的執行期。

Vec:讀取 / 改 / 搬走

// 讀:借用元素,不移動所有權
let v = vec!["a".to_string(), "b".to_string()];
for s in v.iter() {           // &String -> &str(自動解參考)
    println!("{}", s);
}

// 改:唯一可變借用
let mut nums = vec![1, 2, 3];
for n in nums.iter_mut() {    // &mut i32
    *n *= 2;
}

// 搬:移動所有權並消耗集合
let words = vec!["x".to_string(), "y".to_string()];
let moved: Vec<String> = words.into_iter().collect();
// words 不再可用;moved 擁有元素

HashMap:把「鍵的身份證」和「值的居留證」講清楚

雜湊表的最大陷阱,不在哈希函式,而在「鍵與值到底歸誰」:

  • 插入時:鍵和值的所有權都移交給 HashMap。它現在是主人,負責最後銷毀。

  • 查詢時:你只需要看,不需要擁有;所以只拿借用的視圖即可。

這裡有個精妙之處:String&str 的共存哲學。

  • String(身份證):擁有完整的資料所有權,可以活很久。適合放在 HashMap 裡長期持有。

  • &str(居留證):只是一個暫時的「視圖」或「引用」,生命週期很短。適合用在查詢這種臨時場景。
    https://ithelp.ithome.com.tw/upload/images/20250926/201244628fVapVX1q8.png

當你用一個 &str 去查詢 HashMap 裡由 String 持有的鍵時,Rust 知道它們本質上可以匹配。你不需要為了查詢,額外建立一個新的 String

這就是「最小承諾原則」的體現:需要長期持有就用 String,只需要臨時查看就用 &str

HashMap:插入 / 查詢(借用) / 更新

use std::collections::HashMap;

let mut m: HashMap<String, i32> = HashMap::new();

// 插入:移交所有權
m.insert("user:1".to_string(), 1);

// 查詢:以 &str 借用查 String 鍵(無分配)
if let Some(v) = m.get("user:1") {   // &i32
    println!("{}", v);
}

// 更新:entry 收斂為單一路徑
let counter = m.entry("user:1".to_string()).or_insert(0); // &mut i32
*counter += 1;

以型別,取代 runtime 的 if/else

在許多語言中,我們習慣寫大量的防御式 if/else 來處理各種不確定性:

  • 這個指標會不會是 null

  • 這份資料是否會被其他地方意外修改?

  • 物件在這裡被釋放了嗎?還能安全使用嗎?

Rust 的哲學是將這些執行期的擔憂,提前到編譯期解決。
它透過所有權系統,讓「不該發生」的情況在型別層面就無法通過編譯:

  • 所有權 (Ownership)借用 (Borrowing)可變借用 (Mutable Borrowing) 的三分法,將資料的使用規則刻在型別上。

  • 當錯誤的狀態根本無法被表示,處理這些「特殊情況」的 if/else 分支自然也就不需要了。

簡單來說,與其在每個路口設立警告牌,不如直接設計一條不會走錯的路。

clone():一場關於成本的坦白

當你在程式碼中寫下 clone(),你其實是在做一個明確的聲明:

  1. 我清楚知道我需要一份全新的資料所有權。

  2. 我願意為此支付對應的成本(記憶體配置、複製時間)。

如果 clone() 隨處可見,通常是設計不良的警訊。
Rust 的集合(Collections)正是約束 clone() 的最佳工具,它強迫我們思考操作的真實意圖:

  • 讀取:使用零成本的「借用 (&T)」。

  • 修改:使用受控的「可變借用 (&mut T)」。

  • 轉移:使用語義明確的「移動 (Move)」。

我們會發現,clone() 的最佳時機,是資料跨越邊界(線程、函數回傳)且所有權確實需要被複製時。

資料操作的決策清單

  1. 只是讀取資料? -> 優先使用「借用 (&T)」。

  2. 需要就地修改? -> 取得唯一的「可變借用 (&mut T)」。

  3. 要把所有權交出去? -> 直接「移動 (Move)」。

  4. 需要長期持有字串?String僅需短期檢視?&str

  5. 看到 clone() 時反問自己:這是邊界處理的必要之惡,還是設計上的便宜行事?

結論:不只是 API,更是設計語言

Rust 的集合不僅是資料容器,更是不變式 (Invariants) 的執法者

  • 它用所有權釐清資料的生命週期。

  • 它讓每一次複製的成本都無比清晰。

  • 它將潛在的錯誤,從執行期的 Bug,轉化為編譯期的型別錯誤。

當你擁抱這種嚴謹的簡潔,VecHashMap 就不再只是一份 API 清單,而是一種聲明資料關係的語言。

Rust 的美學,是將複雜性交給編譯器,將可預測性留給你。

延伸閱讀


上一篇
(Day11) Rust 智慧指標(Smart pointers):從所有權到安全併發
系列文
Rust 逼我成為更好的工程師:從 Borrow Checker 看軟體設計12
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言