Rust 入门:编译器是你最严格的朋友 书架

第 10 章 / 共 12 章

第十章 · 集合与迭代器

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 循环,括号里放什么,决定了元素以什么身份交到你手上——这正是第四、五章规则的日常应用:

在别的语言里「遍历的同时往集合里塞东西」是著名翻车现场(迭代器失效);Rust 里它编译不过——迭代器拿着只读纸条期间,你申请不到可改纸条,第五章的排他规矩自动护驾。

工具箱齐了:Vec 装一排,HashMap 装一柜,迭代器负责流水作业。但你可能注意到一个问题:map 能处理 Vec<i32> 也能处理 Vec<String>,println! 什么类型都能打印——这些「什么类型都行」的能力是怎么写出来的?答案是泛型和 trait,下一章见。

Rust 入门:编译器是你最严格的朋友 · 王建硕
费曼式写法 · 由多个 AI 代理撰写与互相审校