前面型別篇講過固定大小的 array、所有權篇講過 String 與 &str 的差別。這一篇把三個最常用的 heap 集合一次串完:Vec<T>、String、HashMap<K, V>——它們會在所有 Rust 程式裡反覆出現。重點不只在 API,更在這些型別跟所有權、借用、Option 的互動。
三個集合都把資料放在 heap 上,可以動態增長:
| 型別 | 說明 |
|---|---|
Vec<T> | 同型別動態陣列 |
String | UTF-8 動態字串(本質上是 Vec<u8> 的封裝) |
HashMap<K, V> | 雜湊表 |
Vec<T>
建立與基本操作
// 空 Vec
let v: Vec<i32> = Vec::new();
// 巨集,最常用
let v = vec![1, 2, 3];
// 預先分配容量(已知大小時優化)
let mut v: Vec<i32> = Vec::with_capacity(100);
// 推入 / 取出
let mut v = vec![1, 2, 3];
v.push(4); // [1, 2, 3, 4]
let last = v.pop(); // Some(4),v = [1, 2, 3]
v.insert(0, 0); // [0, 1, 2, 3]
v.remove(0); // 回傳 0,v = [1, 2, 3]
// 長度
println!("{}", v.len()); // 3
println!("{}", v.is_empty()); // false
讀取元素:兩種方式
let v = vec![10, 20, 30];
// 1. 索引:越界會 panic
let x = v[1]; // 20
// let bad = v[99]; // panic
// 2. .get():越界回傳 None
let x: Option<&i32> = v.get(1); // Some(&20)
let bad = v.get(99); // None
怎麼選:確定不會越界就用索引(簡潔),不確定就用 .get() + match。
走訪
let v = vec![1, 2, 3];
// 不可變借用
for x in &v {
println!("{x}");
}
// 可變借用(要修改)
let mut v = vec![1, 2, 3];
for x in &mut v {
*x *= 2; // 解參考後修改
}
// v = [2, 4, 6]
// 取走所有權(v 之後不能用)
for x in v {
println!("{x}");
}
iterator 鏈
let v = vec![1, 2, 3, 4, 5];
let doubled: Vec<i32> = v.iter().map(|x| x * 2).collect();
let even: Vec<i32> = v.iter().filter(|&&x| x % 2 == 0).copied().collect();
let sum: i32 = v.iter().sum();
let max = v.iter().max(); // Option<&i32>
.iter() 產生 &i32,到了 .filter() 裡又多包一層變成 &&i32(參考的參考)。.copied() 把 &i32 複製回 i32——i32 是 Copy,這步是免費的;換成 String 就要用 .cloned()。
三種起手式:.iter() 借用、.iter_mut() 可變借用、.into_iter() 取走所有權。
借用規則在 Vec 上的具體表現
let mut v = vec![1, 2, 3];
let first = &v[0]; // 不可變借用
v.push(4); // ❌ 試圖可變借用,但 first 還活著
println!("{first}");
為什麼這不能編?因為 push 在容量不夠時會觸發重新分配,first 原本指的記憶體位址就失效了,變成懸空指標。Rust 在編譯期就擋下來。
具體來說,Vec 容量滿時 push 會在 heap 上配置更大的空間、把舊資料整段搬過去、釋放舊空間。所以指向 Vec 內元素的指標在 push 後可能失效——這就是借用規則禁止「同時持有 &vec 又呼叫 vec.push()」的理由。
String
String 是可變、可增長、UTF-8 編碼的字串。本質是 Vec<u8>,但多了一條保證:內容一定是合法 UTF-8。
建立
let s = String::new();
let s = String::from("hello");
let s = "hello".to_string();
let s = "hello".to_owned();
let s = format!("{} {}", "hello", "world");
修改
let mut s = String::from("hello");
s.push_str(", world"); // 接一個 &str
s.push('!'); // 接一個 char
// + 串接(會 move 左邊的 String)
let s1 = String::from("hello, ");
let s2 = String::from("world");
let s3 = s1 + &s2; // s1 被 move,之後不能用
// 等同呼叫 fn add(self, &str) -> String
// format! 不會 move 任何輸入,較安全
let s1 = String::from("hello, ");
let s2 = String::from("world");
let s3 = format!("{s1}{s2}");
println!("{s1} {s2} {s3}"); // 三個都還能用
UTF-8 的陷阱:不能直接用索引
let s = String::from("hello");
let c = s[0]; // ❌ 編譯錯誤:String 沒實作 Index<usize>
為什麼禁止?因為 String 是 UTF-8,一個「字元」不一定是一個 byte。例如:
let s = String::from("héllo"); // é 占 2 bytes
println!("{}", s.len()); // 6,不是 5
如果允許 s[1],要回傳 byte 還是字元?回傳 byte 會切到字元中間,產生無效 UTF-8。Rust 乾脆禁掉這個操作。
正確走訪字串
let s = String::from("héllo");
// 走訪 char(Unicode scalar value)
for c in s.chars() {
println!("{c}"); // h, é, l, l, o
}
// 走訪 byte
for b in s.bytes() {
println!("{b}"); // 104, 195, 169, 108, 108, 111
}
// 字串切片:要切在合法 UTF-8 邊界,否則 panic
let hello = &s[0..1]; // "h"
// let bad = &s[0..2]; // panic:byte 1 是 é 的中間
String vs &str
String | &str | |
|---|---|---|
| 擁有 / 借用 | 擁有 | 借用 |
| 在哪裡 | heap | 通常指向程式裡的字面值或 String 內部 |
| 可變 | 是(要 mut) | 否 |
| 何時用 | 需要修改 / 持有 | 唯讀傳遞 |
函式參數通常宣告成 &str:&String 可以自動 deref 成 &str,所以收 &str 的函式兩種都吃,適用面更廣。
再補一句來源:字串字面值 "hello" 的型別是 &'static str,指向編譯後 binary 內的唯讀區;String 擁有 heap 上的資料,可以增長修改;&str 是字串切片,可以指向 String 的一部分,生命週期必須 來源。
HashMap<K, V>
key-value 表。要先 use:
use std::collections::HashMap;
let mut scores: HashMap<String, i32> = HashMap::new();
scores.insert(String::from("blue"), 10);
scores.insert(String::from("red"), 20);
讀取
let team = String::from("blue");
let score: Option<&i32> = scores.get(&team);
match score {
Some(s) => println!("{s}"),
None => println!("not found"),
}
// 帶預設值
let s = scores.get(&team).copied().unwrap_or(0);
走訪
for (k, v) in &scores {
println!("{k}: {v}");
}
注意 HashMap 不保證走訪順序,每次跑出來的順序可能不一樣。
更新
let mut scores = HashMap::new();
scores.insert("blue", 10);
// 1. 直接覆蓋
scores.insert("blue", 25);
// 2. 不存在才插入
scores.entry("yellow").or_insert(50);
scores.entry("blue").or_insert(50); // blue 已存在,不變
// 3. 根據舊值更新(or_insert 回傳 &mut V)
let count = scores.entry("blue").or_insert(0);
*count += 1;
entry API 是 HashMap 最好用的部分,計數就是經典場景:
let text = "the quick brown fox jumps over the lazy dog the";
let mut counts: HashMap<&str, i32> = HashMap::new();
for word in text.split_whitespace() {
*counts.entry(word).or_insert(0) += 1;
}
println!("{counts:?}");
// {"the": 3, "quick": 1, "brown": 1, ...}
所有權注意事項
只要 key 和 value 不是 Copy,insert 就會把它們 move 進去:
let key = String::from("name");
let val = String::from("jeremy");
let mut map = HashMap::new();
map.insert(key, val);
// println!("{key} {val}"); // ❌ key, val 已被 move
想保留原變數就改存引用,但 HashMap 的型別參數也要跟著改:
let mut map: HashMap<&str, &str> = HashMap::new();
let key = String::from("name");
map.insert(&key, "jeremy");
// 需注意 key 的生命週期至少要活到 map 被丟掉之後
HashMap 的雜湊與安全性
HashMap 預設用 SipHash,抗 HashDoS 攻擊——安全,但比較慢。如果資料純內部使用、不怕被惡意餵 key,可以換更快的雜湊:
# Cargo.toml
[dependencies]
ahash = "0.8"
use ahash::AHashMap;
let mut map: AHashMap<String, i32> = AHashMap::new();
BTreeMap:要排序時用它
HashMap 不保證順序。要按 key 排序就用 BTreeMap:
use std::collections::BTreeMap;
let mut map = BTreeMap::new();
map.insert("c", 3);
map.insert("a", 1);
map.insert("b", 2);
for (k, v) in &map {
println!("{k}: {v}"); // a, b, c 順序
}
速查:Vec / String / HashMap 對照
| 操作 | Vec | String | HashMap |
|---|---|---|---|
| 建立 | vec![] / Vec::new() | String::new() / String::from() | HashMap::new() |
| 加入 | push(v) | push_str / push | insert(k, v) |
| 取出 | pop() / remove(i) | — | remove(&k) |
| 讀取 | v[i] / v.get(i) | s.chars() / s.bytes() | m.get(&k) |
| 長度 | len() | len()(byte 數) | len() |
| 走訪 | for x in &v | for c in s.chars() | for (k, v) in &m |
集合篇結束。下一篇進模組系統 mod 與 use,講怎麼把目前散在 main.rs 的程式碼切成多檔案、怎麼控制可見性。
Latest Updates
- 2026.06.11 Content updated
