外观
第 11 章 给宠物收藏册升级
小螃蟹说
第 7 章的收藏册,功能都是手写循环干出来的:想找猫,for 一圈圈看;想数数,for 一圈圈加。循环没错,但写多了你会发现,它们都长一个样——"挨个看看,符合条件的留下"。
这一章,我们给收藏册装上两个新武器:闭包(closure)和迭代器(iterator)。它们俩是一对搭档,能把手写循环变成一句"像英文一样"的话:
rust
pets.iter().filter(|pet| pet.cuteness >= min).collect()读作:"把宠物一个个递出来,留下可爱度够的,收成一串。" 是不是很像英语句子?
这一章还会顺便验证一个传说:迭代器写出来的代码,和手写循环一样快(Rust 的编译器会把它优化回循环)。又短又快,这种好事可不常有。
11.1 预览:我们要做什么
给第 7 章的收藏册加三个新功能:
- 筛选宠物:输入最低可爱度,列出达标的宠物
- 可爱度排行榜:按可爱度从高到低排好
- 收藏成员名单:所有名字串成一句
运行结果
text
选一个:5
最低可爱度(1~10):7
小咪:可爱度 9/10
旺财:可爱度 7/10
皮皮:可爱度 8/10
选一个:6
== 可爱度排行榜 ==
第 1 名:小咪 (可爱度 9/10)
第 2 名:皮皮 (可爱度 8/10)
第 3 名:旺财 (可爱度 7/10)
第 4 名:圆圆 (可爱度 5/10)
选一个:7
收藏成员:小咪、旺财、圆圆、皮皮这一章你会学到:
| 知识 | 是什么 | 会用在哪儿 |
|---|---|---|
| 闭包 | 装在变量里的函数 | |pet| pet.cuteness >= min |
| 迭代器 | 一件件递东西的排队窗口 | pets.iter() |
filter | 留下符合条件的 | 筛选宠物 |
map | 把每件东西加工一下 | 取出名字 |
collect | 把结果收成集合 | 收成一串 |
sort_by | 用闭包当裁判排序 | 排行榜 |
11.2 动手做
步骤一:回到收藏册
这一章继续用第 7 章的 pet_collection 项目(没有存档功能,保持简单):
bash
cd pet_collection先回忆一下第 7 章的手写循环长什么样——find_by_species 就是标准款:
rust
fn find_by_species(pets: &Vec<Pet>) {
// ...
let mut found = 0;
for pet in pets.iter() {
if pet.species == species {
found += 1;
println!("{}:{} 岁", pet.name, pet.age);
}
}
}"挨个看看,是猫就打印"——循环本身没毛病,但我们马上会看到,同样的意思,用迭代器写能短一半,还不会数错 found。
步骤二:认识闭包
先做个小实验。把 src/main.rs 的 main 临时改成:
rust
fn main() {
let add_one = |number| number + 1;
println!("3 加 1 等于 {}", add_one(3));
println!("10 加 1 等于 {}", add_one(10));
}运行:
运行结果
text
3 加 1 等于 4
10 加 1 等于 10|number| number + 1 就是一个闭包(closure)。解剖一下:
- 两道竖线
|number|是"参数栏"——闭包的参数写在竖线里 number + 1是闭包的身体(就一个表达式,连大括号都不用)- 整个闭包装进了变量
add_one——函数被当成了值,想存哪儿存哪儿
和第 1 章的 fn 函数对比:
rust
fn add_one_fn(number: u32) -> u32 { // 必须写全:名字、参数类型、返回类型
number + 1
}
let add_one = |number| number + 1; // 闭包:连类型都不用写函数要报户口(名字、类型全写上);闭包是匿名的小工,抓到就用,类型靠猜。Rust 能猜出 number 是数字——因为 + 1 需要数字。
闭包最厉害的地方是:它能"看见"外面的变量(这叫捕获环境)。这个我们马上在筛选里用到。
小贴士
把第 3 章的 count_chars、第 8 章的 read_input 这些函数名字回想一下——它们都是"有名字的工人"。闭包是"随叫随到的小工",不用起名字,用完就忘。什么时候用哪个?要反复用、要写名字 → fn;临时小活、一句话的事 → 闭包。
步骤三:筛选宠物(filter)
现在来真的。在 main.rs 里加第一个新功能:
rust
fn filter_by_cuteness(pets: &Vec<Pet>) {
println!();
let min = read_number("最低可爱度(1~10):");
let found: Vec<&Pet> = pets
.iter()
.filter(|pet| pet.cuteness >= min)
.collect();
if found.is_empty() {
println!("没有可爱度 >= {} 的宠物。", min);
return;
}
for pet in found {
println!("{}:可爱度 {}/10", pet.name, pet.cuteness);
}
}菜单加两项(第 5 项),match 加分支:
rust
println!("5. 筛选宠物(按可爱度)");rust
"5" => filter_by_cuteness(&pets),这一小段是三句"英文":
pets.iter():把宠物一件件递出来(排队窗口).filter(|pet| pet.cuteness >= min):留下可爱度 ≥ min 的。这里用了闭包当筛子——注意闭包"看见"了外面的min变量!这就是捕获环境.collect():把筛出来的收成一串(Vec<&Pet>)
第 7 章的手写版要写 found 计数器、for 循环、if 判断;现在一句 filter 搞定,还不会数错。
闭包捕获环境,fn 做不到
filter 的闭包用到了 min——它是闭包外面、函数里的变量。fn 函数是"看不见"外面的(它只能靠参数);闭包天生就"看得见"。这就是为什么 filter 用闭包而不是 fn:筛子需要知道最低可爱度是多少,而这个数是外面传来的。
运行试试:
运行结果
text
选一个:5
最低可爱度(1~10):7
小咪:可爱度 9/10
旺财:可爱度 7/10
皮皮:可爱度 8/10步骤四:可爱度排行榜(sort_by)
加第二个功能——按可爱度从高到低排队:
rust
fn cuteness_ranking(pets: &Vec<Pet>) {
println!();
if pets.is_empty() {
println!("收藏册还是空的。");
return;
}
let mut ranked: Vec<&Pet> = pets.iter().collect();
ranked.sort_by(|a, b| b.cuteness.cmp(&a.cuteness));
println!("== 可爱度排行榜 ==");
for (index, pet) in ranked.iter().enumerate() {
println!("第 {} 名:{} (可爱度 {}/10)", index + 1, pet.name, pet.cuteness);
}
}菜单第 6 项、match 加 "6" => cuteness_ranking(&pets),。
新朋友是 sort_by:排序时,它会给闭包递来两个宠物(a 和 b),闭包的任务是当裁判——告诉它俩谁排前面。裁判用 cmp 比(第 2 章的老朋友!Ordering 三种结果):
rust
ranked.sort_by(|a, b| b.cuteness.cmp(&a.cuteness));a.cuteness.cmp(&b.cuteness)是"从小到大"排b.cuteness.cmp(&a.cuteness)把两个数反过来比,就变成"从大到小"——排行榜要最可爱的当第 1 名,所以要反过来
注意 ranked 是 Vec<&Pet>——装的不是宠物,是宠物们的借条(第 3 章引用)。排序只是排借条的顺序,宠物本体一个没动。
小贴士
sort_by 还有一个懒人兄弟 sort_by_key:如果你只想"按某个字段排",写 sort_by_key(|pet| pet.cuteness) 就行,它自动从小到大。想倒序?排完再 reverse()(第 9 章排行榜的招数)。两种都行,sort_by 更灵活,sort_by_key 更短。
步骤五:收藏成员名单(map + collect)
第 7 章练习三,我们用循环版写过名单:
rust
// 第 7 章的循环版
let mut names: Vec<String> = Vec::new();
for pet in pets.iter() {
names.push(pet.name.clone());
}现在用 map 一行搞定:
rust
fn roll_call(pets: &Vec<Pet>) {
println!();
if pets.is_empty() {
println!("收藏册还是空的。");
return;
}
let names: Vec<String> = pets.iter().map(|pet| pet.name.clone()).collect();
println!("收藏成员:{}", names.join("、"));
}菜单第 7 项、match 加 "7" => roll_call(&pets),。
map 是"加工流水线":每件递进来的东西,过一遍闭包,变成新东西出去。这里每只宠物过一遍 |pet| pet.name.clone(),变成它的名字(克隆一份,别抢走宠物的名字——第 7 章老规矩)。
对比循环版和 map 版:
rust
// 循环版:5 行
let mut names: Vec<String> = Vec::new();
for pet in pets.iter() {
names.push(pet.name.clone());
}
// map 版:1 行
let names: Vec<String> = pets.iter().map(|pet| pet.name.clone()).collect();同一个意思,5 行变 1 行。循环版容易犯的错(忘了 mut、push 错了变量),map 版根本没机会犯。
步骤六:整合
菜单、函数都齐了,运行完整玩一遍:
运行结果
text
选一个:5
最低可爱度(1~10):7
小咪:可爱度 9/10
旺财:可爱度 7/10
皮皮:可爱度 8/10
选一个:6
== 可爱度排行榜 ==
第 1 名:小咪 (可爱度 9/10)
第 2 名:皮皮 (可爱度 8/10)
第 3 名:旺财 (可爱度 7/10)
第 4 名:圆圆 (可爱度 5/10)
选一个:7
收藏成员:小咪、旺财、圆圆、皮皮升级完成!现在收藏册有了三个"英文句子级"的功能。闭上眼睛回想一下:iter 递、filter 筛、map 加工、collect 收——这一套组合拳,是 Rust 程序员的看家本领,以后写任何程序都会用到。
11.3 知识深挖
11.3.1 闭包:装在变量里的函数
闭包(closure)是可以存进变量、可以传给别人当参数的"匿名小函数"。
rust
let add_one = |number| number + 1; // 最简单的闭包
let add = |a, b| a + b; // 两个参数
let greet = || println!("你好!"); // 没有参数,两道空竖线三个特点,记住就够用:
- 参数在竖线里,身体紧跟其后:
|参数| 身体。身体只有一个表达式时,连大括号都省了 - 类型全靠猜:
fn要写参数类型、返回类型,闭包不用——编译器从用法里猜 - 能看见外面的世界:闭包可以捕获定义它的作用域里的变量(这就是它和
fn最大的区别)
捕获的三种姿势,和第 3 章的借用规则一一对应:
- 借来看(不可变借用):
filter里pet.cuteness >= min——只是读min - 借来改(可变借用):闭包里
count += 1之类的修改 - 整个拿走(移动):闭包被送进新线程时(第 15 章,加
move关键字)
Rust 会自动选择最省事的姿势:能借看就不借改,能借改就不拿走。我们只需要知道"闭包能看见外面"就够了。
11.3.2 迭代器:排队窗口
迭代器(iterator)是"一件件递东西"的机制。第 3 章我们就说过排队窗口的比喻,现在正式认识它:
rust
let mut iter = pets.iter(); // 窗口开张,宠物们排队
iter.next(); // 递出第一只
iter.next(); // 递出第二只next() 递出一只,递完所有再 next(),返回 None(第 5 章的老朋友)——"队伍空了"。
迭代器有个重要性格:懒。光创建它,什么事都不会发生;只有 next() 或者 collect() 这类"消费"动作,它才真正干活。就像排队窗口:窗口立着,没人取件,包裹就躺着不动。
我们常用的都是借用迭代器 pets.iter()(只借不看)。它还有两个兄弟:
pets.iter_mut():可变借用(要改宠物时用)pets.into_iter():整个拿走(宠物归迭代器所有了)
11.3.3 适配器与消费方法
迭代器的方法分两大家族:
适配器(adaptor):产生新迭代器,不改动原数据:
| 方法 | 干什么 |
|---|---|
filter(闭包) | 留下闭包说"对"的 |
map(闭包) | 每件加工一下,变成新东西 |
take(n) | 只要前 n 件 |
skip(n) | 跳过前 n 件 |
消费方法(consuming):真的动手,把迭代器用掉:
| 方法 | 干什么 |
|---|---|
collect() | 收成一串(Vec) |
sum() | 全部加起来 |
count() | 数有几件 |
for 循环 | 一件件拿出来用 |
注意适配器是懒的:filter、map 自己不会动手,只有后面接上消费方法(通常就是 collect),整条流水线才真正运转。所以这条链:
rust
pets.iter()
.filter(|pet| pet.cuteness >= min) // 1. 筛(懒)
.map(|pet| pet.name.clone()) // 2. 加工(懒)
.collect() // 3. 收(开工!)读起来像一条工厂流水线:递进来 → 筛选 → 加工 → 装箱。每一步只写"要什么",不写"怎么循环"——循环的细节,迭代器替你干了。
11.3.4 为什么用迭代器?
三个理由,一个比一个硬:
- 代码像英文,一眼看懂。
filter+map+collect是"声明式"的:你说要什么,不说怎么找。手写循环是"命令式"的:你得亲手写let mut、for、if、push——细节一多,就看不到重点了 - 少犯错。手写循环的经典 bug:数错
found、忘加mut、越界访问。迭代器把这些全包了——你只管说"留下符合条件的" - 性能一样快。你可能会担心"这么多方法调用,是不是变慢了?"不会!Rust 的编译器会把迭代器链优化回手写循环——这叫零成本抽象(zero-cost abstraction):写起来高级,跑起来和低级写法一样快。官方书上有一句名言:"闭包和迭代器的速度,超乎你的想象。"
什么时候还用 for 循环?
迭代器不是万能药。有些场景,for 循环反而更清楚——比如要在循环中途退出、要和外面的变量频繁互动。记住:能用迭代器就用,用着别扭就换 for。两种都会,才是完整的 Rust 程序员。
11.4 动脑筋练习
三个练习都是给收藏册再加一个新功能(菜单第 8 项),而且都要求用迭代器,不许手写循环!
练习一:可爱度总和与平均
加一个 cute_summary:打印所有宠物可爱度的总和与平均。
提示:老规矩,map 把每只宠物的可爱度取出来,然后……数它的 sum()(消费方法)!平均 = 总和 ÷ 数量,记得 as f64 和 {:.1}(第 9 章的招数)。别忘了空收藏册的检查。
点开看答案
菜单加 println!("8. 可爱度总和与平均");,match 加 "8" => cute_summary(&pets),。
新函数:
rust
fn cute_summary(pets: &Vec<Pet>) {
println!();
if pets.is_empty() {
println!("收藏册还是空的。");
return;
}
let total: u32 = pets.iter().map(|pet| pet.cuteness).sum();
let count = pets.len() as u32;
println!("可爱度总和:{},平均:{:.1}", total, total as f64 / count as f64);
}流水线:iter() 递宠物 → map(|pet| pet.cuteness) 取出可爱度 → sum() 全部相加。四只宠物可爱度 9、7、5、8,总和 29,平均 7.2。
注意 let total: u32 = ... 的 : u32——sum() 能加出好几种数字类型,得告诉它"我要 u32"。
练习二:最可爱宠物奖(迭代器版)
第 7 章练习二,我们用手写循环做过"最可爱宠物"。这次用迭代器!提示:迭代器有个方法叫 max_by——它和 sort_by 一样递来两个宠物当裁判,但只留最厉害的那个,返回 Option<&Pet>。
点开看答案
菜单加 println!("8. 最可爱宠物奖");,match 加 "8" => most_cute(&pets),。
新函数:
rust
fn most_cute(pets: &Vec<Pet>) {
println!();
match pets.iter().max_by(|a, b| a.cuteness.cmp(&b.cuteness)) {
Some(winner) => println!("最可爱的宠物是 {}!可爱度 {}/10", winner.name, winner.cuteness),
None => println!("收藏册还是空的。"),
}
}max_by 自动完成"擂台赛":递来两只,裁判用闭包比可爱度,比到最后,全场的擂主装在 Option 里交出来——Some(冠军) 或 None(空收藏册)。
对比第 7 章练习二的手写版:那时候我们要 best_index、best_cuteness、for 循环、if 判断,十行;现在一个 max_by,三行。而且 None 分支(空收藏册)自动处理——手写版要是忘了空检查,早就崩溃了。
练习三:找名字里的字
加一个 find_by_name_word:输入一个字,列出所有名字里带这个字的宠物。
提示:filter + String::contains(第 3 章用过"行里有没有这个词"):pet.name.contains(&word)——word 是外面的变量,闭包捕获它(这正是 filter 的标准用法)。
点开看答案
菜单加 println!("8. 找名字里的字");,match 加 "8" => find_by_name_word(&pets),。
新函数:
rust
fn find_by_name_word(pets: &Vec<Pet>) {
println!();
let word = read_input("名字里要含什么字?");
let found: Vec<&Pet> = pets
.iter()
.filter(|pet| pet.name.contains(&word))
.collect();
if found.is_empty() {
println!("没有名字里带 {} 的宠物。", word);
return;
}
for pet in found {
println!("{}:{} 岁的小{}", pet.name, pet.age, pet.species);
}
}输入"小",收藏册里叫"小咪"的就被筛出来了。
看这个闭包:|pet| pet.name.contains(&word)——它捕获了外面的 word(你刚输入的字)。还记得 fn 函数做不到这个吗?"筛子需要知道条件,而条件来自外面"——这正是闭包的主场。 小咪、旺财、圆圆、皮皮,四个名字,一个 filter,搞定。
11.5 完整代码清单
项目结构:
text
pet_collection/
├── Cargo.toml
└── src/
└── main.rs文件:Cargo.toml
toml
[package]
name = "pet_collection"
version = "0.1.0"
edition = "2024"文件:src/main.rs
rust
use std::collections::HashMap;
use std::io;
struct Pet {
name: String,
species: String,
age: u32,
cuteness: u32,
}
fn main() {
let mut pets: Vec<Pet> = Vec::new();
println!("欢迎来到宠物收藏册!");
loop {
println!();
println!("== 菜单 ==");
println!("1. 添加宠物");
println!("2. 查看所有宠物");
println!("3. 按种类找宠物");
println!("4. 统计报告");
println!("5. 筛选宠物(按可爱度)");
println!("6. 可爱度排行榜");
println!("7. 收藏成员名单");
println!("输入 0 退出");
let choice = read_input("选一个:");
match choice.as_str() {
"1" => add_pet(&mut pets),
"2" => list_all(&pets),
"3" => find_by_species(&pets),
"4" => show_stats(&pets),
"5" => filter_by_cuteness(&pets),
"6" => cuteness_ranking(&pets),
"7" => roll_call(&pets),
"0" => break,
_ => println!("没有这个选项哦。"),
}
}
println!("再见!欢迎下次再来玩!");
}
fn filter_by_cuteness(pets: &Vec<Pet>) {
println!();
let min = read_number("最低可爱度(1~10):");
let found: Vec<&Pet> = pets
.iter()
.filter(|pet| pet.cuteness >= min)
.collect();
if found.is_empty() {
println!("没有可爱度 >= {} 的宠物。", min);
return;
}
for pet in found {
println!("{}:可爱度 {}/10", pet.name, pet.cuteness);
}
}
fn cuteness_ranking(pets: &Vec<Pet>) {
println!();
if pets.is_empty() {
println!("收藏册还是空的。");
return;
}
let mut ranked: Vec<&Pet> = pets.iter().collect();
ranked.sort_by(|a, b| b.cuteness.cmp(&a.cuteness));
println!("== 可爱度排行榜 ==");
for (index, pet) in ranked.iter().enumerate() {
println!("第 {} 名:{} (可爱度 {}/10)", index + 1, pet.name, pet.cuteness);
}
}
fn roll_call(pets: &Vec<Pet>) {
println!();
if pets.is_empty() {
println!("收藏册还是空的。");
return;
}
let names: Vec<String> = pets.iter().map(|pet| pet.name.clone()).collect();
println!("收藏成员:{}", names.join("、"));
}
fn add_pet(pets: &mut Vec<Pet>) {
println!();
let name = read_input("宠物的名字:");
let species = read_input("宠物的种类:");
let age = read_number("宠物的年龄:");
let cuteness = read_number("可爱度(1~10):");
pets.push(Pet {
name,
species,
age,
cuteness,
});
println!("已加入收藏册!现在一共有 {} 只宠物。", pets.len());
}
fn list_all(pets: &Vec<Pet>) {
println!();
if pets.is_empty() {
println!("收藏册还是空的,先添加一只宠物吧。");
return;
}
for (index, pet) in pets.iter().enumerate() {
println!(
"{}. {} 是 {} 岁的小{},可爱度 {}/10",
index + 1,
pet.name,
pet.age,
pet.species,
pet.cuteness
);
}
}
fn find_by_species(pets: &Vec<Pet>) {
println!();
let species = read_input("想找什么种类的宠物?");
let mut found = 0;
for pet in pets.iter() {
if pet.species == species {
found += 1;
println!("{}:{} 岁,可爱度 {}/10", pet.name, pet.age, pet.cuteness);
}
}
if found == 0 {
println!("没有找到 {} 呢。", species);
}
}
fn show_stats(pets: &Vec<Pet>) {
println!();
println!("一共有 {} 只宠物。", pets.len());
let mut counts: HashMap<String, u32> = HashMap::new();
for pet in pets.iter() {
let count = counts.entry(pet.species.clone()).or_insert(0);
*count += 1;
}
for (species, count) in &counts {
println!("{}:{} 只", species, count);
}
}
fn read_number(prompt: &str) -> u32 {
loop {
let input = read_input(prompt);
match input.parse() {
Ok(number) => return number,
Err(_) => println!("要输入数字哦。"),
}
}
}
fn read_input(prompt: &str) -> String {
println!("{}", prompt);
let mut input = String::new();
io::stdin().read_line(&mut input).expect("读取输入失败");
input.trim().to_string()
}怎么运行:
bash
cd pet_collection
cargo run运行检查单:
筛选:输入最低可爱度,列出所有达标的宠物
筛选:没有宠物达标时,有礼貌的提示
排行榜:可爱度从高到低,第一名是最可爱的
名单:名字用"、"串成一句
空收藏册时,三个新功能都不崩溃,都有提示
把第 7 章的 find_by_species 改成迭代器版(提示:filter + collect),跑起来结果一样