本节摘要:介绍三大主力集合 Vec、String、HashMap 的创建、增删查改惯用法,重点讲清两件事:迭代时不转移所有权(借用法),以及"在遍历中修改集合"这一高频败诉案件的合规改法。读完你能按访问模式选对集合并写出遍历安全的代码。
let mut exhibits = Vec::new(); // 动态数组 exhibits.push("指纹"); exhibits.push("鞋印"); let first = exhibits[0]; // 越界会 panic let safe = exhibits.get(5); // 返回 None,不崩 let mut title = String::from("卷宗"); title.push_str("173"); title.push('号'); use std::collections::HashMap; let mut map = HashMap::new(); map.insert("办案人", "张法官"); if let Some(who) = map.get("办案人") { println!("负责人:{}", who); }
| 集合 | 擅长 | 代价 |
|---|---|---|
| Vec | 顺序存取、按下标 | 中间插入搬移元素 |
| String | 可增长 UTF-8 文本 | 不支持按字节下标索引(须用字符迭代) |
| HashMap | 键值查找近常数 | 无序、键须可哈希 |
push 传入的是值的所有权——元素进保管室,原件失效;for 遍历默认也拿所有权。只想查看时借用:
for e in &exhibits { print!("{} ", e); } // 借用,exhibits 仍可用 for e in &mut exhibits { e.push_str("!"); } // 可变借用,就地改
for e in &exhibits { if e.len() > 2 { exhibits.push("新证物"); } // E0502 }
遍历持有只读借用,push 要可变借用,铁律冲突。合规改法三选:先收集再修改(collect 成新 Vec 后 extend)、用索引 while 循环并控制下标、或改用 drain 与 retain 这类"按规则批量处理"的 API。retain 尤其适合过滤场景:exhibits.retain(|e| e.len() <= 2);
| 容器 | 语义 | 索引复杂度 | 顺序 | 典型场景 |
|---|---|---|---|---|
| Vec | 连续栈式 | O(1) 尾部 push/pop | 插入序 | 默认选择 |
| VecDeque | 双端队列 | O(1) 两端 | 队列序 | 滑动窗口、任务队列 |
| HashMap | 哈希映射 | 均摊 O(1) | 无序 | 键值查找 |
| BTreeMap | 有序映射 | O(log n) | 键有序 | 范围查询、可迭代序 |
判定次序建议:先 Vec,除非被两种需求之一逼走——需要按键查找(换 Map),需要两端进出(换 Deque)。Map 内部再按"是否需要有序遍历或范围查询"选 B 树版。哈希版默认使用抗 DoS 的 SipHash,键查询安全但偏慢;键是整数或短字符串且为内部数据时,可在依赖里换哈希器提速,这属于发布期优化,不在初稿考虑。
"查不到就插入"是保管室最高频的业务,朴素写法会因借用重叠被驳回,entry 是法定解。
use std::collections::HashMap; fn main() { let mut tally: HashMap<String, u32> = HashMap::new(); let words = ["盗", "窃", "盗", "赃", "窃", "盗"]; for w in words { *tally.entry(w.to_string()).or_insert(0) += 1; // 单次可变借用完成查改 } for (k, v) in &tally { println!("{} 出现 {} 次", k, v); } // 输出:盗 3 次 / 窃 2 次 / 赃 1 次(HashMap 遍历序不定) }
对照写法 if let Some(n) = tally.get_mut(k) { *n += 1 } else { tally.insert(...) }:功能相同,但两次借用之间理论上别的线程可改表(单线程下编译器仍会放行前一版吗——会,但可读性已输)。entry 把"检查与写入"合并成一个原子步骤,语义与性能双赢。
String 是持有堆缓冲的产权证,&str 是指向 UTF-8 字节序列的旁听证。中文处理最容易踩的坑是按字节索引:"证物甲".len() 得 9(字节),而 "证物甲".chars().count() 得 3。字符串不支持 s[1] 这种写法,E0600 出庭,因为字节边界可能切开一个字符。需要第 n 个字符的正确姿势是 s.chars().nth(n),返回 Option——越界是正常可能而不是异常。切子串用 &s[a..b] 时若 a、b 不在字符边界上,运行期 panic。这些约束的共同源头只有一条:Rust 把字符串定义为 UTF-8 字节序列,不做隐式编解码。
fn main() { let shelf = vec![String::from("甲001"), String::from("乙002"), String::from("甲003")]; let jia: Vec<String> = shelf .into_iter() // ① 转移产权的迭代 .filter(|s| s.starts_with("甲")) // ② 惰性筛 .map(|s| format!("[{}]", s)) // ② 惰性变换 .collect(); // ③ 收尾物化 println!("{:?}", jia); // ["[甲001]", "[甲003]"] }
iter / iter_mut / into_iter 分别对应借读、借写、夺取三种迭代形态,collect 决定最终容器与错误策略。三段式"转换→惰性操作→物化"是集合处理的通用骨架,性能上与手写循环等价(零成本抽象,编译器会展开),可读性却高出一档。保管室一节的所有案例,事后都值得改写成三段式再对照一遍。