Rust 练习题
读 Rust 很有用,但真正让知识留下来的是动手写。这一页有 18 道小题,按主题分组,由易到难排列。
每道题都先读题目,自己动手试一试,然后再打开答案。卡住了、看到编译错误都完全没关系,这本来就是学习的一部分。而且 Rust 的错误信息往往就是你能得到的最好提示。
怎么练习:在另一个标签页打开 Rust Playground,或者用 cargo new practice 新建一个项目(见快速上手)。在那里写下你的答案,再和参考答案对比。如果写法不同但输出一样,那多半也没问题。好的写法几乎总是不止一种。
1. FizzBuzz
Section titled “1. FizzBuzz”打印 1 到 15 的数字。但遇到 3 的倍数时打印 Fizz,遇到 5 的倍数时打印 Buzz,同时是 3 和 5 的倍数时打印 FizzBuzz。
提示:用 for 循环遍历 1..=15,再配合 %(取余)运算符。记得先检查“同时是两者的倍数”。
查看答案
fn main() { for n in 1..=15 { if n % 15 == 0 { println!("FizzBuzz"); } else if n % 3 == 0 { println!("Fizz"); } else if n % 5 == 0 { println!("Buzz"); } else { println!("{n}"); } }}输出:
12Fizz4BuzzFizz78FizzBuzz11Fizz1314FizzBuzz顺序很重要。如果先检查 n % 3 == 0,那么 15 会打印 Fizz,永远走不到 FizzBuzz 那个分支。能同时被 3 和 5 整除的数,就是能被 15 整除的数。
2. 反转字符串
Section titled “2. 反转字符串”写一个函数 reverse(s: &str) -> String,返回倒过来的文本。"stressed" 应该变成 "desserts"。
提示:.chars() 会逐个给出字符,.rev() 把迭代器反过来,.collect() 可以拼出一个 String。
查看答案
fn reverse(s: &str) -> String { s.chars().rev().collect()}
fn main() { println!("{}", reverse("stressed")); println!("{}", reverse("hello")); println!("{}", reverse("año"));}输出:
dessertsollehoña为什么用 .chars() 而不是字节?Rust 的 String 是 UTF-8 编码的,像 ñ 这样的字符占两个字节(汉字更是占三个)。直接反转字节会把它拆坏,变成无效的文本。.chars() 按完整的字符来处理。更多内容见字符串。
3. 数元音
Section titled “3. 数元音”写 count_vowels(s: &str) -> usize,统计字符串里元音字母(a、e、i、o、u)的个数。大写和小写都算。
提示:.chars().filter(...).count()。像 "aeiouAEIOU" 这样的字符串有一个 .contains(c) 方法,可以用来检查每个字符。
查看答案
fn count_vowels(s: &str) -> usize { s.chars().filter(|c| "aeiouAEIOU".contains(*c)).count()}
fn main() { println!("{}", count_vowels("Hello, World")); println!("{}", count_vowels("Rust is fun")); println!("{}", count_vowels("AEIOU and sometimes y")); println!("{}", count_vowels("rhythm"));}输出:
33100filter 传给闭包的是每个元素的引用(&char),所以要写 *c 才能拿到 char 本身。闭包和迭代器两页会详细讲这种写法。
所有权与借用
Section titled “所有权与借用”4. 修复移动错误
Section titled “4. 修复移动错误”下面的程序编译不通过。只修改 shout 函数的参数和两处调用,让它打印两次 HELLO!。
fn shout(text: String) { println!("{}!", text.to_uppercase());}
fn main() { let word = String::from("hello"); shout(word); shout(word);}error[E0382]: use of moved value: `word` --> src/main.rs:8:11 |6 | let word = String::from("hello"); | ---- move occurs because `word` has type `String`, which does not implement the `Copy` trait7 | shout(word); | ---- value moved here8 | shout(word); | ^^^^ value used here after move编译器说:word 的值已经被移动走了,之后又被使用了一次。
提示:shout 只需要看一看这段文本,并不需要拥有它。见所有权。
查看答案
fn shout(text: &str) { println!("{}!", text.to_uppercase());}
fn main() { let word = String::from("hello"); shout(&word); shout(&word);}输出:
HELLO!HELLO!函数参数是 String 时,调用它就会把字符串移动进去,main 之后就不能再用了。改成 &str,函数就只是借用它。编译器还会建议用 word.clone(),这样也行,但它会毫无必要地复制整个字符串。这里借用才是更好的修法。
5. 修复借用错误
Section titled “5. 修复借用错误”这个程序也编译不通过。让它打印 Ana,同时还要把 Ben 加进列表。
fn main() { let mut names = vec![String::from("Ana")]; let first = &names[0]; names.push(String::from("Ben")); println!("{first}");}error[E0502]: cannot borrow `names` as mutable because it is also borrowed as immutable --> src/main.rs:4:5 |3 | let first = &names[0]; | ----- immutable borrow occurs here4 | names.push(String::from("Ben")); | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ mutable borrow occurs here5 | println!("{first}"); | ----- immutable borrow later used here编译器说:names 已经被不可变地借用了,所以不能再把它可变地借用。
提示:问题出在顺序上。push 修改 vector 的时候,first 还在使用中。见借用。
查看答案
fn main() { let mut names = vec![String::from("Ana")]; names.push(String::from("Ben")); let first = &names[0]; println!("{first}");}输出:
Ana为什么 Rust 在这里这么严格?push 可能需要更多空间,这时 vector 会把所有元素搬到内存里一块更大的新位置。那么 first 就会指向旧位置,而旧位置已经无效了。Rust 的规则(可以有多个读者,或者一个写者,但两者不能同时存在)在程序运行之前就挡住了这个 bug。把引用放到 push 之后再取,问题就解决了。
6. 字符串的第一个单词
Section titled “6. 字符串的第一个单词”写 first_word(s: &str) -> &str,返回第一个单词(第一个空格之前的所有内容)。如果没有空格,就返回整个字符串。不要新建 String,而是返回原字符串的一个 slice。
提示:s.find(' ') 返回 Option<usize>,里面是第一个空格的位置。&s[..i] 是从开头到 i 的 slice。
查看答案
fn first_word(s: &str) -> &str { match s.find(' ') { Some(i) => &s[..i], None => s, }}
fn main() { println!("{:?}", first_word("hello world")); println!("{:?}", first_word("Rust")); println!("{:?}", first_word(""));}输出:
"hello""Rust"""返回的 &str 和输入指向同一块内存,所以什么都没有复制。编译器知道结果不会比输入活得更久。这就是生命周期省略在起作用:只有一个引用进、一个引用出时,你不用自己写生命周期。
更短的写法是 s.split_whitespace().next().unwrap_or(""),它还会跳过开头多余的空格。
7. vector 中最大的数
Section titled “7. vector 中最大的数”写 largest(numbers: &[i32]) -> Option<i32>。它返回最大的数,列表为空时返回 None。这次不要用内置的 .max(),自己写循环。
提示:先把第一个元素当作“目前最大的”。numbers.first() 返回 Option,正好帮你处理了空列表的情况。
查看答案
fn largest(numbers: &[i32]) -> Option<i32> { let mut biggest = *numbers.first()?; for &n in numbers { if n > biggest { biggest = n; } } Some(biggest)}
fn main() { println!("{:?}", largest(&[3, 41, 7, 12])); println!("{:?}", largest(&[-5, -2, -9])); println!("{:?}", largest(&[]));}输出:
Some(41)Some(-2)None列表为空时,numbers.first()? 会提前返回 None。这就是函数返回 Option 的原因:“什么都没有时的最大值”没有合理的答案,Rust 要你把这一点明确写出来。在实际代码里,numbers.iter().max() 能做同样的事。
用 0 而不是第一个元素作为初始值,是一个经典 bug:列表 [-5, -2, -9] 会错误地得到 0。
8. 单词频率
Section titled “8. 单词频率”统计一句话里每个单词出现了多少次。忽略大小写和标点,并按字母顺序打印单词。
提示:用 BTreeMap<String, usize>。它用起来像 HashMap,但会让键保持有序。map.entry(word).or_insert(0) 会给你一个计数器,往上加 1 就行。
查看答案
use std::collections::BTreeMap;
fn main() { let text = "The cat sat on the mat. The end!"; let mut counts: BTreeMap<String, usize> = BTreeMap::new();
for word in text.split_whitespace() { let clean = word .trim_matches(|c: char| !c.is_alphanumeric()) .to_lowercase(); if clean.is_empty() { continue; } *counts.entry(clean).or_insert(0) += 1; }
for (word, count) in &counts { println!("{word}: {count}"); }}输出:
cat: 1end: 1mat: 1on: 1sat: 1the: 3为什么用 BTreeMap 而不是 HashMap?HashMap 给出元素的顺序是不可预测的,每次运行都可能不一样。如果需要有序的输出,就用 BTreeMap,或者把 HashMap 收集到一个 Vec 里再排序。
trim_matches 去掉了 mat. 里的 . 和 end! 里的 !,to_lowercase 让 The 和 the 变成同一个单词。
这里的句子保留了英文:按空格切分单词、按字母排序、区分大小写,这些都是英文文本才有的情况,中文句子里的词之间并没有空格。
9. 偶数的平方
Section titled “9. 偶数的平方”给定 vec![1, 2, 3, 4, 5, 6, 7, 8],构建一个新的 vector,只包含偶数的平方:[4, 16, 36, 64]。然后打印它们的和。用迭代器方法,不要用 for 循环。
提示:把 .iter()、.filter(...)、.map(...) 和 .collect() 串起来。然后用 .iter().sum() 求和。
查看答案
fn main() { let numbers = vec![1, 2, 3, 4, 5, 6, 7, 8];
let squares: Vec<i32> = numbers .iter() .filter(|n| *n % 2 == 0) .map(|n| n * n) .collect();
let total: i32 = squares.iter().sum();
println!("{:?}", squares); println!("总和:{total}");}输出:
[4, 16, 36, 64]总和:120像读菜谱一样从上往下读这条链:拿到这些数,留下偶数,每个求平方,放进一个 Vec。迭代器是惰性的,在 .collect() 要结果之前,什么都不会真正执行。见迭代器。
10. 解析并求和
Section titled “10. 解析并求和”写 sum_numbers(input: &str) -> Result<i32, ParseIntError>。它接收像 "1, 2, 3" 这样的文本,解析其中每个数字并返回总和。只要有一段不是数字,就返回错误,而不是让程序崩溃。
提示:按 ',' 切分,对每一段调用 .trim(),再调用 .parse()。用 ? 运算符在出错时提前返回。
查看答案
use std::num::ParseIntError;
fn sum_numbers(input: &str) -> Result<i32, ParseIntError> { let mut total = 0; for piece in input.split(',') { let n: i32 = piece.trim().parse()?; total += n; } Ok(total)}
fn main() { for input in ["1, 2, 3", "10,20, 30", "4, five, 6", ""] { match sum_numbers(input) { Ok(sum) => println!("{input:?} -> 总和是 {sum}"), Err(e) => println!("{input:?} -> 错误:{e}"), } }}输出:
"1, 2, 3" -> 总和是 6"10,20, 30" -> 总和是 60"4, five, 6" -> 错误:invalid digit found in string"" -> 错误:cannot parse integer from empty string错误信息本身(invalid digit found in string 表示“字符串里有无效的数字”,cannot parse integer from empty string 表示“无法把空字符串解析成整数”)来自标准库,所以是英文。
? 一个字符做了两件事:如果 parse() 给出 Ok(n),你就拿到 n;如果给出 Err(e),函数立刻返回 Err(e)。这也是为什么这个函数本身也必须返回 Result。
附加题:Result 也能配合 .sum() 使用,所以下面这一行做的是同样的事,并且会在遇到第一个错误时停下:
input.split(',').map(|p| p.trim().parse::<i32>()).sum()11. Option 链式调用
Section titled “11. Option 链式调用”每个用户可能有邮箱,也可能没有。写 email_domain(user: &User) -> Option<&str>,返回 @ 后面的部分。没有邮箱,或者邮箱里没有 @ 时,返回 None。然后按名字查找用户,打印他们的域名。
提示:user.email.as_deref() 把 Option<String> 转成 Option<&str>。email.split_once('@') 返回 Option<(&str, &str)>。? 运算符对 Option 也有效。见 Option。
查看答案
struct User { name: String, email: Option<String>,}
fn email_domain(user: &User) -> Option<&str> { let email = user.email.as_deref()?; let (_, domain) = email.split_once('@')?; Some(domain)}
fn main() { let users = vec![ User { name: String::from("Ben"), email: None }, User { name: String::from("Cleo"), email: Some(String::from("cleo-at-home")) }, ];
for name in ["Ana", "Ben", "Cleo", "Dan"] { let domain = users .iter() .find(|u| u.name == name) .and_then(email_domain) .unwrap_or("(没有域名)"); println!("{name}: {domain}"); }}输出:
Ana: example.comBen: (没有域名)Cleo: (没有域名)Dan: (没有域名)这里有三种不同的“失败”:Dan 不存在,Ben 没有邮箱,Cleo 的邮箱里没有 @。每一种都是一个 Option 步骤,? 或 .and_then() 会在遇到第一个 None 时停下。不需要 if 检查,也不会有空指针错误。最后的 .unwrap_or(...) 负责给出默认值。
12. 银行账户
Section titled “12. 银行账户”创建一个 Account 结构体,包含户主和余额。给它加上 deposit(amount) 和 withdraw(amount) 两个方法。withdraw 必须返回 Result<u64, String>:成功时返回新余额,钱不够时返回错误信息。余额永远不能低于零。
提示:会修改账户的方法要接收 &mut self。先检查余额,再做减法。见结构体和 Result。
查看答案
struct Account { owner: String, balance: u64,}
impl Account { fn new(owner: &str) -> Account { Account { owner: owner.to_string(), balance: 0 } }
fn deposit(&mut self, amount: u64) { self.balance += amount; }
fn withdraw(&mut self, amount: u64) -> Result<u64, String> { if amount > self.balance { return Err(format!( "无法取出 {amount}:余额只有 {}", self.balance )); } self.balance -= amount; Ok(self.balance) }}
fn main() { let mut account = Account::new("Ana"); account.deposit(100);
for amount in [30, 50, 40] { match account.withdraw(amount) { Ok(left) => println!("取出 {amount},还剩 {left}"), Err(e) => println!("错误:{e}"), } }
println!("最后 {} 的余额是 {}", account.owner, account.balance);}输出:
取出 30,还剩 70取出 50,还剩 20错误:无法取出 40:余额只有 20最后 Ana 的余额是 20用 u64(无符号整数)是一层额外的安全网:它根本存不下负数余额。如果你忘了检查,self.balance -= amount 在 debug 模式下会以 “attempt to subtract with overflow”(减法溢出)panic,而不是悄悄变成负数。
trait 与泛型
Section titled “trait 与泛型”13. Shape trait
Section titled “13. Shape trait”定义一个 trait Shape,包含两个方法:area(&self) -> f64 和 name(&self) -> String。为 Circle(有半径)和 Rectangle(有宽和高)实现它。把两种形状放进同一个 Vec,打印每个形状的名字和面积,保留 2 位小数。
提示:要在一个 Vec 里存放不同的类型,用 Vec<Box<dyn Shape>>。见 trait 和 trait 对象。{:.2} 会打印 2 位小数。
查看答案
trait Shape { fn area(&self) -> f64; fn name(&self) -> String;}
struct Circle { radius: f64,}
struct Rectangle { width: f64, height: f64,}
impl Shape for Circle { fn area(&self) -> f64 { std::f64::consts::PI * self.radius * self.radius } fn name(&self) -> String { format!("圆(r = {})", self.radius) }}
impl Shape for Rectangle { fn area(&self) -> f64 { self.width * self.height } fn name(&self) -> String { format!("长方形({} x {})", self.width, self.height) }}
fn main() { let shapes: Vec<Box<dyn Shape>> = vec![ Box::new(Circle { radius: 1.0 }), Box::new(Rectangle { width: 3.0, height: 4.0 }), Box::new(Circle { radius: 2.5 }), ];
let mut total = 0.0; for shape in &shapes { println!("{}:{:.2}", shape.name(), shape.area()); total += shape.area(); } println!("总面积:{total:.2}");}输出:
圆(r = 1):3.14长方形(3 x 4):12.00圆(r = 2.5):19.63总面积:34.78循环并不知道、也不关心手里拿的是哪种形状。它只知道每个元素“是一个 Shape”,所以可以调用 area() 和 name()。以后要加一个 Triangle,只需要再写一个 impl,循环一行都不用改。
14. 泛型版的 largest
Section titled “14. 泛型版的 largest”回到第 7 题,但这次让它适用于任何可以比较的类型:整数、小数、字符和字符串。函数签名是 largest<T: PartialOrd>(items: &[T]) -> Option<&T>。
提示:T: PartialOrd 的意思是“T 支持 < 和 >”。返回一个引用(&T),这样就不需要复制或克隆元素。见泛型。
查看答案
fn largest<T: PartialOrd>(items: &[T]) -> Option<&T> { let mut biggest = items.first()?; for item in items { if item > biggest { biggest = item; } } Some(biggest)}
fn main() { println!("{:?}", largest(&[3, 41, 7, 12])); println!("{:?}", largest(&[2.5, -1.0, 9.75])); println!("{:?}", largest(&['r', 'u', 's', 't'])); println!("{:?}", largest(&["pear", "apple", "zucchini"]));
let empty: Vec<i32> = Vec::new(); println!("{:?}", largest(&empty));}输出:
Some(41)Some(9.75)Some('u')Some("zucchini")None函数体和第 7 题几乎一样,真正的变化只有 <T: PartialOrd> 这一部分。没有这个约束,编译器会拒绝 item > biggest,因为并不是所有类型都能比较。字符串按字母顺序比较,所以 "zucchini"(西葫芦)最大。
注意空 vector 上的 Vec<i32>:没有任何元素时,Rust 猜不出 T 是什么,所以你得告诉它。
HashSet、排序、模式与迭代器
Section titled “HashSet、排序、模式与迭代器”15. 找出重复的名字
Section titled “15. 找出重复的名字”一份客人名单里,有些名字写了不止一次。写 find_repeats(words: &[&str]) -> Vec<String>,返回所有出现超过一次的名字,每个只列一次,按字母顺序排列。另外再打印一共有多少位不同的客人。
提示:HashSet 能记住你已经见过哪些值。它的 insert 方法在值已经存在时返回 false。HashSet 没有固定的顺序,所以最后要对结果排序。
查看答案
use std::collections::HashSet;
fn find_repeats(words: &[&str]) -> Vec<String> { let mut seen = HashSet::new(); let mut repeats = HashSet::new();
for &word in words { // 如果这个名字已经在集合里,insert 会返回 false if !seen.insert(word) { repeats.insert(word); } }
let mut result: Vec<String> = repeats.into_iter().map(String::from).collect(); result.sort(); result}
fn main() { let guests = ["ana", "ben", "cleo", "ben", "dan", "ana", "ben"]; println!("重复的名字:{:?}", find_repeats(&guests));
let unique: HashSet<&str> = guests.iter().copied().collect(); println!("不同的客人:{} 位", unique.len());
println!("重复的名字:{:?}", find_repeats(&["x", "y", "z"]));}输出:
重复的名字:["ana", "ben"]不同的客人:4 位重复的名字:[]为什么要两个集合?seen 存放目前为止见过的每个名字,repeats 存放第二次遇到的名字。"ben" 出现了三次,但因为 repeats 是一个集合,它只会被存一次。
收集到 HashSet 里也是统计不同值个数最快的办法:重复的值会自动消失。如果没有 result.sort() 这一行,重复名字的顺序在每次运行时都可能不同。
16. 按长度给单词排序
Section titled “16. 按长度给单词排序”把一组单词从短到长排序。两个单词长度相同时,按 A 到 Z 的顺序排列,并且忽略大小写。然后把你的结果和普通的 .sort() 比较一下。
提示:sort_by 接收一个闭包,闭包比较两个元素并返回一个 Ordering。.then_with(...) 加上第二个比较条件,只在前一个条件打平时才起作用。见排序。
查看答案
fn main() { let mut words = vec!["pear", "Fig", "banana", "apple", "kiwi", "Cherry", "date"];
let mut plain = words.clone(); plain.sort(); println!("普通排序: {plain:?}");
words.sort_by(|a, b| { a.len() .cmp(&b.len()) .then_with(|| a.to_lowercase().cmp(&b.to_lowercase())) }); println!("按长度排序:{words:?}");}输出:
普通排序: ["Cherry", "Fig", "apple", "banana", "date", "kiwi", "pear"]按长度排序:["Fig", "date", "kiwi", "pear", "apple", "banana", "Cherry"]单词本身保留了英文,因为这道题按 len() 排序,而 len() 数的是字节,不是字符。
看看普通排序的结果:"Cherry" 和 "Fig" 排在最前面。字符串是按字符编码比较的,所有大写字母都排在所有小写字母前面。所以我们在打平时比较的是 to_lowercase() 之后的版本。
同样的规则还有更短的写法:words.sort_by_key(|w| (w.len(), w.to_lowercase()))。元组会逐个字段比较,所以先由长度决定,长度相同时再由小写后的单词决定。
17. 用 match 守卫计算运费
Section titled “17. 用 match 守卫计算运费”为下面这个枚举写 shipping_cost(p: Package) -> String,只用一个 match:
enum Package { Letter { grams: u32 }, Parcel { kg: u32, express: bool }, Pickup,}规则:自提免费。20 克以内的信件 1 美元,更重的信件 3 美元。超过 30 公斤的包裹根本不能寄。加急包裹 10 美元,每公斤再加 2 美元。其他包裹 5 美元,每公斤再加 1 美元。
提示:match 分支可以在模式后面加一个 if,叫做守卫(guard):Package::Letter { grams } if grams <= 20 => ...。分支从上到下依次检查,所以要把“太重”的规则放在加急规则前面。见模式匹配。
查看答案
enum Package { Letter { grams: u32 }, Parcel { kg: u32, express: bool }, Pickup,}
fn shipping_cost(p: Package) -> String { match p { Package::Pickup => String::from("免费(自提)"), Package::Letter { grams } if grams <= 20 => String::from("$1(普通信件)"), Package::Letter { grams } => format!("$3(大信件,{grams} g)"), Package::Parcel { kg, .. } if kg > 30 => format!("太重了,无法寄送({kg} kg)"), Package::Parcel { kg, express: true } => format!("${}(加急)", 10 + kg * 2), Package::Parcel { kg, express: false } => format!("${}(普通包裹)", 5 + kg), }}
fn main() { let packages = vec![ Package::Letter { grams: 15 }, Package::Letter { grams: 80 }, Package::Parcel { kg: 4, express: false }, Package::Parcel { kg: 4, express: true }, Package::Parcel { kg: 35, express: true }, Package::Pickup, ];
for p in packages { println!("{}", shipping_cost(p)); }}输出:
$1(普通信件)$3(大信件,80 g)$9(普通包裹)$18(加急)太重了,无法寄送(35 kg)免费(自提)35 公斤的加急包裹是最有意思的一个。它能匹配两个分支,但只有第一个生效。如果你把加急分支放在“太重”分支上面,它就会高高兴兴地为一个根本寄不出去的包裹收 80 美元。
注意最后一个分支写的是 express: false,而不是 ..。这不是必须的,但能让这个分支明确说出它处理的是哪种情况。编译器仍然会检查是否覆盖了所有情况。守卫不参与这项检查,所以那个不带守卫的 Package::Letter { grams } 分支必须存在。
18. 整理库存报告
Section titled “18. 整理库存报告”一份仓库报告每行一种货物,比如 "apples 12"。有些行是坏的:要么是空行,要么数量不是数字。用迭代器方法(不用 for 循环),只保留能解析并且数量大于零的行。把它们按 name: count 的格式用逗号连起来打印,然后打印总数。
提示:.lines() 把文本切成一行一行。filter_map 只保留闭包返回的 Some 值,所以能一步完成过滤和转换。.ok() 把 Result 转成 Option。见迭代器。
查看答案
fn main() { let report = "apples 12\nbananas x\npears 0\n\ncherries 40\nplums 7";
let in_stock: Vec<(&str, u32)> = report .lines() .filter_map(|line| line.split_once(' ')) .filter_map(|(name, count)| count.parse::<u32>().ok().map(|n| (name, n))) .filter(|&(_, n)| n > 0) .collect();
let list: Vec<String> = in_stock .iter() .map(|(name, n)| format!("{name}: {n}")) .collect(); let total: u32 = in_stock.iter().map(|(_, n)| n).sum();
println!("{}", list.join(", ")); println!("总件数:{total}");}输出:
apples: 12, cherries: 40, plums: 7总件数:59一次跟踪一行。空行里没有空格,所以 split_once 返回 None,第一个 filter_map 就把它丢掉了。"bananas x" 能正常切分,但 "x" 解析不了,所以第二个 filter_map 把它丢掉。"pears 0" 通过了前两关,然后因为数量是零,被 filter 去掉了。
我们只 collect 一次得到一个 Vec,然后遍历它两次:一次拼文本,一次算总数。迭代器只能用一次,所以把清理好的数据存起来,比把所有内容解析两遍更简单。
接下来学什么
Section titled “接下来学什么”如果你做完了全部 18 道题,干得漂亮,这是实实在在的进步。🦀
- Rustlings(英文):官方出品的一组小程序,每个都有点问题,你在终端里逐个修好它们。做完这一页之后,这是很好的下一步。
- Exercism Rust track(英文):大约 100 道带测试的题目,还有人工导师免费点评你的答案。
- 通过例子学 Rust(Rust by Example 中文版):每个主题都有简短、可运行的例子。做某道题卡在语法上时很好用。
Iteratortrait — 标准库文档(英文):所有迭代器方法(filter_map、fold、zip等等),每个都配有一个小例子。HashSet— 标准库文档(英文):所有集合方法,包括union、intersection和difference。