9. 闭包与迭代器篇¶
对应章节:第13章(闭包与迭代器)。
本篇掌握函数式编程风格:闭包捕获、迭代器链、集合统计。
项目 16:文本分析管道¶
目标:用闭包 + 迭代器链处理文本,统计词频。
创建项目¶
说明:这是库项目(--lib),核心逻辑放
src/lib.rs。如要命令行演示,可自行添加src/main.rs。
功能要求¶
- 读取文本文件
total_words、distinct_words、longest_wordtop_words(text, n, min_len):统计词频,返回前 n 个min_len用参数过滤过短单词
知识点¶
| 知识点 | 说明 |
|---|---|
| 迭代器链 | map/filter/collect |
| 闭包 | 匿名函数捕获外部变量 |
HashMap 计数 |
entry().or_insert(0) += 1 |
HashSet |
去重 |
.sort_by |
多级排序 |
.take(n) |
截取前 n 个 |
代码¶
src/lib.rs:
use std::collections::HashMap;
// 总词数:按空白切分计数
pub fn total_words(text: &str) -> usize {
text.split_whitespace().count()
}
// 不同单词数:转小写后放入 HashSet(自动去重)
pub fn distinct_words(text: &str) -> usize {
text.split_whitespace()
.map(|w| w.to_lowercase())
.collect::<std::collections::HashSet<_>>()
.len()
}
// 最长单词:max_by_key 按长度取最大
pub fn longest_word(text: &str) -> &str {
text.split_whitespace()
.max_by_key(|w| w.len())
.unwrap_or("")
}
/// 统计词频,返回按次数降序的 (单词, 次数)
/// min_len:只统计长度 >= min_len 的单词
pub fn word_freq(text: &str, min_len: usize) -> Vec<(String, usize)> {
let mut counts: HashMap<String, usize> = HashMap::new();
for word in text.split_whitespace() {
let word = word.to_lowercase(); // 统一小写,Rust 和 rust 计为同一词
// 闭包/条件捕获外部变量 min_len(此处是 if 条件)
if word.len() >= min_len {
// entry().or_insert(0):没有则插 0,有则取原有值
// * 解引用后 += 1 完成计数
*counts.entry(word).or_insert(0) += 1;
}
}
let mut vec: Vec<_> = counts.into_iter().collect();
// 多级排序:先按次数降序(b.1.cmp(&a.1)),同次数的按字母升序(then_with)
vec.sort_by(|a, b| b.1.cmp(&a.1).then_with(|| a.0.cmp(&b.0)));
vec
}
pub fn top_words(text: &str, n: usize, min_len: usize) -> Vec<(String, usize)> {
// 惰性迭代器:into_iter().take(n) 只在 collect 时真正执行
word_freq(text, min_len)
.into_iter()
.take(n)
.collect()
}
src/main.rs:
use std::env;
use std::fs;
use text_analyzer::{distinct_words, longest_word, top_words, total_words};
fn main() {
let args: Vec<String> = env::args().collect();
let filename = match args.get(1) {
Some(f) => f,
None => {
eprintln!("用法:cargo run -- <文件名> [min_len]");
std::process::exit(1);
}
};
// 可选参数:第 2 个参数解析成 min_len,缺省为 3
let min_len: usize = args.get(2).and_then(|s| s.parse().ok()).unwrap_or(3);
let text = fs::read_to_string(filename).expect("读取文件失败");
println!("总词数:{}", total_words(&text));
println!("不同单词数:{}", distinct_words(&text));
println!("最长单词:{}", longest_word(&text));
println!("\nTop 5 高频词(长度 ≥ {}):", min_len);
for (word, count) in top_words(&text, 5, min_len) {
println!(" {}: {}", word, count);
}
}
测试文件 poem.txt(10 行):
Rust is a language
rust makes programming fast
safe and expressive
programming with rust is fun
and learning rust is easy
rust is the best
and fast is good
and safe is better
and code is clean
rust rocks
运行¶
cargo run -- poem.txt
# 总词数:47
# 不同单词数:35
# 最长单词:programming
# Top 5:rust:7, and:4, the:2, programming:2, is:2(示例数据不同则结果不同)
注意事项¶
- 闭包捕获
min_len:word.len() >= min_len中的min_len是外部变量,闭包按引用捕获,无需 move。 entry(word).or_insert(0) += 1:HashMap 计数惯用法——没有则插 0,有则*解引用加 1。sort_by(|a, b| b.1.cmp(&a.1).then_with(...)):先按次数降序(b.1.cmp(&a.1)),再按单词升序(then_with)——多级排序链。- 迭代器是惰性的:
split_whitespace().filter(...).collect()只在collect时真正执行——一步到位构建管道。 to_lowercase():词频统计前统一小写,Rust和rust计为同一词。
进阶:标志参数与 Option 化 API¶
设计差异:
--min-len标志:不用位置参数,而是cargo run -- poem.txt --min-len 3的标志风格longest_word返回Option<(&str, usize)>:空文本返回 None 而不是空串,把"可能没有结果"编码进类型distinct_words不用 HashSet:复用 word_count 的 HashMap 取 len- 完整单元测试(6 个),库的正确性不依赖手工运行
src/lib.rs:
use std::collections::HashMap;
pub fn word_count(text: &str) -> HashMap<String, usize> {
let mut counts = HashMap::new();
for word in text.split_whitespace() {
*counts.entry(word.to_lowercase()).or_insert(0) += 1;
}
counts
}
pub fn total_words(text: &str) -> usize {
text.split_whitespace().count()
}
// 不同单词数 = 词频表的键数(HashMap 键天然去重)
pub fn distinct_words(text: &str) -> usize {
word_count(text).len()
}
pub fn longest_word(text: &str) -> Option<(&str, usize)> {
text.split_whitespace()
.max_by(|a, b| a.len().cmp(&b.len())) // 按长度比较取最大
.map(|w| (w, w.len())) // 有结果才包装成 Some
}
// 按词频从高到低排序,返回前 n 个(单词,次数)
pub fn top_words(text: &str, n: usize, min_len: usize) -> Vec<(String, usize)> {
let mut counts = HashMap::new();
text.split_whitespace()
.map(|w| w.to_lowercase())
.filter(|w| w.len() >= min_len)
.for_each(|w| {
*counts.entry(w).or_insert(0) += 1;
});
let mut sorted: Vec<(String, usize)> = counts.into_iter().collect();
sorted.sort_by(|a, b| b.1.cmp(&a.1)); // 次数降序
sorted.truncate(n); // 截取前 n 个
sorted
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn counts_words() {
let counts = word_count("rust rust go");
assert_eq!(counts.get("rust"), Some(&2));
assert_eq!(counts.get("go"), Some(&1));
}
#[test]
fn total_and_distinct() {
let text = "a b c a";
assert_eq!(total_words(text), 4);
assert_eq!(distinct_words(text), 3);
}
#[test]
fn longest_word_find_max() {
let text = "short longer longest";
assert_eq!(longest_word(text), Some(("longest", 7)));
}
#[test]
fn longest_word_empty() {
assert_eq!(longest_word(""), None);
}
#[test]
fn top_words_sorted() {
let text = "rust rust go rust go python";
let top = top_words(text, 2, 1);
assert_eq!(top, vec![("rust".to_string(), 3), ("go".to_string(), 2)]);
}
#[test]
fn top_words_min_len() {
let text = "a bb ccc a bb";
let top = top_words(text, 10, 2);
assert_eq!(top, vec![("bb".to_string(), 2), ("ccc".to_string(), 1)]);
}
}
src/main.rs:
use std::env;
use std::fs;
use text_analyzer::{distinct_words, longest_word, top_words, total_words};
fn main() -> Result<(), Box<dyn std::error::Error>> {
let args: Vec<String> = env::args().collect();
let path = &args[1];
// 标志解析:找到 "--min-len" 的位置,取它后面的参数
// 两层 unwrap_or:解析失败用 1,标志不存在也用 1
let min_len: usize = args
.iter()
.position(|a| a == "--min-len")
.map(|i| args[i + 1].parse().unwrap_or(1))
.unwrap_or(1);
let text = fs::read_to_string(path)?;
println!("总词数:{}", total_words(&text));
println!("不同单词:{}", distinct_words(&text));
if let Some((word, len)) = longest_word(&text) {
println!("最长单词:\"{}\"({} 字符)", word, len);
}
println!("词频 Top 5:");
for (word, count) in top_words(&text, 5, min_len) {
println!(" {}: {}", word, count);
}
Ok(())
}
运行¶
差异对照¶
| 原版 | 进阶版 | |
|---|---|---|
longest_word 返回值 |
&str(空文本给空串) |
Option<(&str, usize)>——调用方必须处理"没有"的情况 |
distinct_words 实现 |
HashSet 去重 |
复用 word_count().len() |
min_len 传参 |
位置参数 args[2] |
--min-len 标志 + position() 解析 |
| 排序截断 | sort_by 多级 + take(n) |
sort_by 单级 + truncate(n) |
要点¶
Option化返回值是 Rust API 设计的重要取向:把边界情况交给类型系统,调用方用if let/unwrap_or显式处理,而不是约定"空串表示没找到"。position()找标志:iter().position(|a| a == "--min-len")返回Option<usize>,配合.map(...).unwrap_or(...)一条链完成"有则解析、无则默认"。真正的 CLI 会用 clap crate,但手写一遍才理解它在替你做什么。