這次要聊的是最常見的集合型別——Vec
與 HashMap
。
在遇見 Rust 之前,我一直覺得集合(Collection),不就是個資料而已嗎?
把東西丟進去、拿出來、算算有幾個,很直覺。
但 Rust 卻給了我一記當頭棒喝,它告訴我:集合不只是容器,它更是一道「責任邊界」。
「責任邊界」是什麼意思?
在 Rust 的世界裡,當你把資料放進一個集合,你其實是在做一個重要的決定:
誰擁有這些資料? 是這個集合嗎?
誰該負責在最後把它們清理乾淨?
誰只能在旁邊看看(讀取)?
誰才有權力動手修改?
最關鍵的是,這份「責任歸屬」不是靠團隊默契或個人自律,而是 由編譯器強制執行的一份合約。
這就是它為何如此可靠。
簡單一句話:Rust 的集合讓「資料的命運」變得可預測、可檢查,甚至在編譯時期就能被證明。
我們對集合最常見的操作是什麼?不是新增或刪除,而是在一段程式邏輯中,「讀取它、修改它、或把它帶走」。
Rust 的 Vec
把這三種互動,直接升級成了三種不同的「關係」:
讀取(借用 Borrow):我只是想看看,不會動手,資料本身還在原地。
修改(可變借用 Mutable Borrow):我要動手了,而且為了安全,此刻只有我能動它。
搬走(移動所有權 Move):這東西現在歸我了,原主人不能再碰。
這不是 API 表面技巧,而是把「資料的三種命運」變成了三個不同的型別關係。
結果是程式碼變得超級直覺:
讀取時不可能意外改到資料。
修改時不用擔心其他地方同時也在改。
把資料搬走後,編譯器會確保你不會再誤用舊的那個。
我們可能會覺得:「哇,這麼嚴格?」
沒錯,這正是重點。
Rust 選擇把複雜度擋在編譯期,換來一個乾淨、可預測的執行期。
// 讀:借用元素,不移動所有權
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
。它現在是主人,負責最後銷毀。
查詢時:你只需要看,不需要擁有;所以只拿借用的視圖即可。
這裡有個精妙之處:String
與 &str
的共存哲學。
String
(身份證):擁有完整的資料所有權,可以活很久。適合放在 HashMap
裡長期持有。
&str
(居留證):只是一個暫時的「視圖」或「引用」,生命週期很短。適合用在查詢這種臨時場景。
當你用一個 &str
去查詢 HashMap
裡由 String
持有的鍵時,Rust 知道它們本質上可以匹配。你不需要為了查詢,額外建立一個新的 String
。
這就是「最小承諾原則」的體現:需要長期持有就用 String
,只需要臨時查看就用 &str
。
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;
在許多語言中,我們習慣寫大量的防御式 if/else
來處理各種不確定性:
這個指標會不會是 null
?
這份資料是否會被其他地方意外修改?
物件在這裡被釋放了嗎?還能安全使用嗎?
Rust 的哲學是將這些執行期的擔憂,提前到編譯期解決。
它透過所有權系統,讓「不該發生」的情況在型別層面就無法通過編譯:
所有權 (Ownership)、借用 (Borrowing) 與 可變借用 (Mutable Borrowing) 的三分法,將資料的使用規則刻在型別上。
當錯誤的狀態根本無法被表示,處理這些「特殊情況」的 if/else
分支自然也就不需要了。
簡單來說,與其在每個路口設立警告牌,不如直接設計一條不會走錯的路。
clone()
:一場關於成本的坦白當你在程式碼中寫下 clone()
,你其實是在做一個明確的聲明:
我清楚知道我需要一份全新的資料所有權。
我願意為此支付對應的成本(記憶體配置、複製時間)。
如果 clone()
隨處可見,通常是設計不良的警訊。
Rust 的集合(Collections)正是約束 clone()
的最佳工具,它強迫我們思考操作的真實意圖:
讀取:使用零成本的「借用 (&T
)」。
修改:使用受控的「可變借用 (&mut T
)」。
轉移:使用語義明確的「移動 (Move)」。
我們會發現,clone()
的最佳時機,是資料跨越邊界(線程、函數回傳)且所有權確實需要被複製時。
只是讀取資料? -> 優先使用「借用 (&T
)」。
需要就地修改? -> 取得唯一的「可變借用 (&mut T
)」。
要把所有權交出去? -> 直接「移動 (Move)」。
需要長期持有字串? 用 String
。僅需短期檢視? 用 &str
。
看到 clone()
時反問自己:這是邊界處理的必要之惡,還是設計上的便宜行事?
Rust 的集合不僅是資料容器,更是不變式 (Invariants) 的執法者。
它用所有權釐清資料的生命週期。
它讓每一次複製的成本都無比清晰。
它將潛在的錯誤,從執行期的 Bug,轉化為編譯期的型別錯誤。
當你擁抱這種嚴謹的簡潔,Vec
與 HashMap
就不再只是一份 API 清單,而是一種聲明資料關係的語言。
Rust 的美學,是將複雜性交給編譯器,將可預測性留給你。