跳转至

9. 闭包与迭代器篇

对应章节:第13章(闭包与迭代器)。
本篇掌握函数式编程风格:闭包捕获、迭代器链、集合统计。


项目 16:文本分析管道

目标:用闭包 + 迭代器链处理文本,统计词频。

创建项目

cargo new text_analyzer --lib
cd text_analyzer

说明:这是库项目(--lib),核心逻辑放 src/lib.rs。如要命令行演示,可自行添加 src/main.rs。

功能要求

  1. 读取文本文件
  2. total_words、distinct_words、longest_word
  3. top_words(text, n, min_len):统计词频,返回前 n 个
  4. 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(示例数据不同则结果不同)

注意事项

  1. 闭包捕获 min_len:word.len() >= min_len 中的 min_len 是外部变量,闭包按引用捕获,无需 move。
  2. entry(word).or_insert(0) += 1:HashMap 计数惯用法——没有则插 0,有则 * 解引用加 1。
  3. sort_by(|a, b| b.1.cmp(&a.1).then_with(...)):先按次数降序(b.1.cmp(&a.1)),再按单词升序(then_with)——多级排序链。
  4. 迭代器是惰性的:split_whitespace().filter(...).collect() 只在 collect 时真正执行——一步到位构建管道。
  5. 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(())
}

运行

cargo test                       # 6 个测试全绿
cargo run -- poem.txt
cargo run -- poem.txt --min-len 4   # 过滤掉短单词再统计

差异对照

原版 进阶版
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)

要点

  1. Option 化返回值是 Rust API 设计的重要取向:把边界情况交给类型系统,调用方用 if let / unwrap_or 显式处理,而不是约定"空串表示没找到"。
  2. position() 找标志:iter().position(|a| a == "--min-len") 返回 Option<usize>,配合 .map(...).unwrap_or(...) 一条链完成"有则解析、无则默认"。真正的 CLI 会用 clap crate,但手写一遍才理解它在替你做什么。

评论