Vec:会自己长大的数组
日常用得最多的集合是 Vec(vector,向量)——大白话是「能自动扩容的数组」。元素在堆上排成连续的一排,栈上还是那张熟悉的提货单。它遵守第四章的全部规矩:Vec 是谁的值,谁离场它就带着所有元素一起归还,一个不漏。
let mut scores = Vec::new(); // 空的;类型靠后面的用法推断
scores.push(90); // 从此编译器知道是 Vec<i32>
scores.push(85);
println!("{}", scores[0]); // 90;越界访问会 panic(第九章说的「立刻停下」)
println!("{:?}", scores.get(10)); // None;get 返回 Option,越界不炸,自己选
两种取元素的姿势对应两种哲学:方括号快但越界就 panic,.get() 返回 Option 把「可能没有」摆成明牌。拿不准用哪个?想想第九章的分场景口诀:「理论上不该越界」用方括号,「越界是正常情况」用 get。
HashMap:键查值的柜子
第二个常客是 HashMap(哈希表):存一堆「键 → 值」对,按键秒查:
use std::collections::HashMap;
let mut prices = HashMap::new();
prices.insert(String::from("苹果"), 5);
prices.insert(String::from("螃蟹"), 40);
match prices.get("苹果") {
Some(p) => println!("苹果 {} 元", p),
None => println!("没这货"),
}
注意 insert 收的是 String 不是 &str——所有权过户进了 map,从此键归 map 管,这正符合「一个值一个主人」:柜子里存的东西,柜子负责归还。get 照例返回 Option。还有个偷懒组合技 prices.entry(key).or_insert(0):「有这个键就给我现有值,没有就插个 0 再给我」,做计数统计时一行顶五行。
迭代器:不写循环的循环
真正的重头戏是迭代器(iterator)。先看传统写法和新写法的对照——把一组分数翻倍再筛出及格的:
let scores = vec![90, 55, 85, 40];
// 新写法:一条流水线
let passed: Vec<i32> = scores
.iter() // 变成迭代器:只读纸条,遍历元素
.map(|x| x * 2) // 每个元素翻倍
.filter(|x| *x >= 60) // 留下及格的
.collect(); // 收拢成新的 Vec
中间 |x| x * 2 这种写法是闭包(closure)——大白话是「临时定义的一行小函数」,竖线里是参数。迭代器方法一个接一个,像流水线工位:map 负责变形,filter 负责把关,collect 负责打包出厂。
有个反直觉但重要的性质:迭代器是惰性的——只写 .map(...).filter(...) 什么都不发生,直到 collect(或别的消费方法)真正来取货,整条流水线才开动,而且只开一遍。编译器甚至会在你误写一条没人消费的流水线时提醒你:这玩意白写了。
零成本抽象第一次露脸:这条流水线编译出来的机器码,和你手写一个 for 循环逐个判断、逐个塞入,一模一样快。map/filter 不是运行时的一层包装,而是编译期就展开、内联、优化掉的「描述」。你写给人看的优雅,不花机器一分钱。
for 循环里的所有权细节
普通的 for 循环,括号里放什么,决定了元素以什么身份交到你手上——这正是第四、五章规则的日常应用:
for x in &v:借来只读看,循环完 v 还能用;for x in &mut v:借来逐个改,v 还是你的;for x in v:整个 Vec 过户给循环,元素一个个被你「消费」,循环完 v 就没了。
在别的语言里「遍历的同时往集合里塞东西」是著名翻车现场(迭代器失效);Rust 里它编译不过——迭代器拿着只读纸条期间,你申请不到可改纸条,第五章的排他规矩自动护驾。
工具箱齐了:Vec 装一排,HashMap 装一柜,迭代器负责流水作业。但你可能注意到一个问题:map 能处理 Vec<i32> 也能处理 Vec<String>,println! 什么类型都能打印——这些「什么类型都行」的能力是怎么写出来的?答案是泛型和 trait,下一章见。