用 itertools 增强迭代器
你会学到什么
- 标准库
Iterator没直接提供、但日常常用的迭代器操作。 itertools的几个高频方法:chunk_by、unique、counts、sorted、join、cartesian_product、chunks,以及izip!宏。- 「扩展 trait(extension trait)」模式——为什么一个
use就能给已有的迭代器凭空加上新方法。 - 把第三方能力封装进库模块,上层只管调用。
这一章建立在迭代器之上:你已经熟悉 map/filter/collect 的链式风格,itertools 只是在同一条链上接更多积木。
核心概念:扩展 trait
标准库把迭代器方法定义在 Iterator trait 上。itertools 没法去修改标准库的源码,它换了个思路:定义自己的 trait Itertools,并为所有实现了 Iterator 的类型一次性实现它:
// itertools 内部大致是这样(简化):
pub trait Itertools: Iterator {
fn unique(self) -> Unique<Self> { /* ... */ }
fn counts(self) -> HashMap<Self::Item, usize> { /* ... */ }
// ……
}
impl<T: Iterator> Itertools for T {}
于是只要把这个 trait 带进作用域:
use itertools::Itertools;
任何迭代器(Vec 的 .iter()、map 链、0..n……)就立刻多出 .unique()、.counts() 等方法。方法本身没改变原类型,是「调用方导入了 trait 才看得见」——这正是 Rust 里给外部类型扩展行为的标准手法,标准库的 Iterator、Itertools、anyhow::Context 都是这个套路。忘了 use 这一行,编译器就会报「方法不存在」,这是最常见的坑。
文件结构
示例按用途拆成两个模块,方便对照阅读:
47_itertools/
├── Cargo.toml # 声明 itertools = "0.14"
└── src/
├── lib.rs # mod 声明 + pub use 重导出
├── grouping.rs # 分组与去重:chunk_by / unique / counts
├── combining.rs # 组合与排列:izip! / sorted / join / cartesian_product / chunks
└── main.rs # 薄壳:调用库函数并打印
lib.rs 把两个模块里的函数 pub use 出来,main.rs 因此能直接 use rt_47_itertools::run_lengths;,感知不到底层用了哪个第三方包。
运行代码
cd examples
cargo run -p rt_47_itertools
cargo test -p rt_47_itertools
代码讲解
分组与去重(grouping.rs)
chunk_by 把相邻且相等的元素压成一组。它在 0.14 由旧名 group_by 改名而来——看老资料时注意这点。要强调的是「相邻」:
// [1, 1, 2, 2, 2, 1] -> [(1, 2), (2, 3), (1, 1)]
for (key, group) in &data.iter().chunk_by(|&&x| x) {
out.push((key, group.count()));
}
末尾那个 1 不会和开头的 1 合并,因为它们中间隔着 2。这跟「先排序再分组」是两回事——chunk_by 做的是游程(run-length)切分。注意写法上要 &...chunk_by(...):分组结果是惰性的,必须在持有它引用的期间把每组消费掉。
unique 去重但保留首次出现的顺序,不像 HashSet 那样打乱次序;counts 直接把迭代器统计成 HashMap<值, 次数>,省掉手写 entry(..).or_insert(0) 的样板。
组合与排列(combining.rs)
izip! 宏一次压缩 3 个以上的迭代器,且产出扁平的 (a, b, c) 而不是标准库 zip 那种嵌套的 ((a, b), c):
izip!(names, ages, cities) // -> (name, age, city)
和 zip 一样,任一序列耗尽就停止。其余几个方法都很直观:
sorted/sorted_by_key:消费迭代器、排好序再交还一个新迭代器,比「collect成Vec再sort」少写一步,链式风格也更连贯。join(sep):用Display把元素拼成字符串,自动处理「最后一个不加分隔符」的边界。cartesian_product:求两个序列的笛卡尔积,结果长度是两者长度之积,常用于「生成所有组合」。chunks(n):每n个切一块,最后一块允许不足;和chunk_by一样是惰性的,要在引用期间收集。
常见错误
忘记导入扩展 trait:
// ❌ 没有 use itertools::Itertools;
let v: Vec<_> = data.iter().unique().collect();
// ^^^^^^ 方法不存在
unique/counts/sorted 都挂在 Itertools trait 上,必须 use itertools::Itertools; 才可见。注意 izip! 是宏,需要单独从 itertools::izip 导入(或用 itertools::izip! 全路径)。
练习
- 用
.tuple_windows()计算一个数列里「每对相邻元素之差」。 - 用
chunk_by把一串单词按首字母分组(提示:先sorted_by_key(首字母),因为chunk_by只合并相邻项)。 - 给
counts的结果排序,找出出现次数最多的元素(提示:into_iter().max_by_key(|(_, n)| *n))。
小结
itertools 通过扩展 trait,给标准迭代器补上了 chunk_by、unique、counts、sorted、join、cartesian_product 等高频操作。理解「use 一个 trait 就能给已有类型加方法」这件事,比记住具体某个方法更有价值——它是 Rust 生态里随处可见的扩展模式。
下一步
到这里,「引用第三方包」这一组(rand、chrono、regex、itertools)就告一段落了:你已经掌握了声明依赖、use 进来、把它封装进自己的库模块这套完整流程。接下来进入 Web 与数据库实战——先看如何用 axum 写一个 Web 服务,再到 SQLite 做持久化。
完整示例代码
下面是 examples/47_itertools/ 的完整源码。无需 clone 仓库,直接在页面上阅读、复制、对照运行。
examples/47_itertools/src/main.rs
//! 一个小演示:调用本 crate 暴露的 itertools 扩展功能,并打印带标签的输出。
//!
//! `main` 只管调用 `rt_47_itertools` 里封装好的函数——这些函数内部用到了
//! 第三方包 `itertools`,但调用方完全感知不到。
use rt_47_itertools::{
into_chunks, join_with, product_pairs, run_lengths, sorted_by_length, unique_keep_order,
word_counts, zip_three,
};
fn main() {
println!("=== itertools:增强迭代器演示 ===\n");
println!("-- 分组与去重 --");
println!("相邻分组(游程): {:?}", run_lengths(&[1, 1, 2, 2, 2, 1]));
println!(
"去重保序: {:?}",
unique_keep_order(&[3, 1, 3, 2, 1, 2])
);
let counts = word_counts(&["a", "b", "a", "c", "a", "b"]);
println!("词频(a 出现): {} 次", counts["a"]);
println!();
println!("-- 组合与排列 --");
let people = zip_three(&["小明", "小红"], &[18, 20], &["北京", "上海"]);
println!("izip! 三路压缩: {people:?}");
println!(
"按长度排序: {:?}",
sorted_by_length(&["bbb", "a", "cc", "dddd"])
);
println!("join 拼接: {}", join_with(&[1, 2, 3], " - "));
println!("笛卡尔积: {:?}", product_pairs(&[1, 2], &["x", "y"]));
println!("每 2 个一块: {:?}", into_chunks(&[1, 2, 3, 4, 5], 2));
} examples/47_itertools/src/lib.rs
//! 用 `itertools` 这个第三方包给标准迭代器「加料」。
//!
//! 标准库的 `Iterator` 已经很强(`map`/`filter`/`fold`……),但有些常见操作
//! 它没有直接提供,比如「按相邻分组」「去重保序」「计数」「拼接成字符串」
//! 「笛卡尔积」。`itertools` 用**扩展 trait** 的方式把这些方法补上:
//! 只要 `use itertools::Itertools;`,所有迭代器就立刻多出这些方法,无需改动
//! 原有类型。
//!
//! 本 crate 按用途分成两个模块:
//! - `grouping`:分组与去重——`chunk_by`、`unique`、`counts`。
//! - `combining`:组合与排列——`izip!`、`sorted`、`join`、`cartesian_product`、`chunks`。
pub mod combining;
pub mod grouping;
// 重新导出常用函数,调用方可以直接 `use rt_47_itertools::run_lengths;`。
pub use combining::{
into_chunks, join_with, product_pairs, sorted_ascending, sorted_by_length, zip_three,
};
pub use grouping::{run_lengths, unique_keep_order, word_counts}; examples/47_itertools/src/combining.rs
//! 组合与排列:把多个迭代器拼到一起、排序、拼接成字符串、求笛卡尔积、按块切分。
//!
//! 这些同样来自扩展 trait `Itertools`,再加上一个宏 `izip!`。
use itertools::{Itertools, izip};
/// 用 `izip!` 同时遍历三个序列,逐项打包成 `(姓名, 年龄, 城市)`。
///
/// 标准库的 `zip` 一次只能拼两个;要拼 3 个以上,`izip!` 宏更直观,
/// 而且不会产生 `((a, b), c)` 这种嵌套元组。任一序列耗尽即停止。
pub fn zip_three<'a>(
names: &[&'a str],
ages: &[u32],
cities: &[&'a str],
) -> Vec<(&'a str, u32, &'a str)> {
izip!(
names.iter().copied(),
ages.iter().copied(),
cities.iter().copied()
)
.collect()
}
/// 排序后收集成 `Vec`。`sorted` 会消费迭代器,内部收集再排序,返回一个新迭代器。
///
/// 当源头不是 `Vec`(比如来自 `filter`/`map` 的链)又想要有序结果时,
/// `.sorted()` 比「先 `collect` 成 `Vec` 再 `sort`」少写一步。
pub fn sorted_ascending(data: &[i32]) -> Vec<i32> {
data.iter().copied().sorted().collect()
}
/// 按指定的「键」排序:这里按字符串长度从短到长。
pub fn sorted_by_length(words: &[&str]) -> Vec<String> {
words
.iter()
.map(|s| s.to_string())
.sorted_by_key(|s| s.len())
.collect()
}
/// 把一串可显示的元素用分隔符拼成一个字符串,类似其他语言的 `join`。
///
/// `join` 内部用 `Display` 格式化每个元素,省去手写 `fold` 或处理「最后一个
/// 不加分隔符」的边界。
pub fn join_with(data: &[i32], sep: &str) -> String {
data.iter().join(sep)
}
/// 求两个序列的笛卡尔积:对左边每个元素,配上右边的每个元素。
///
/// 结果长度是 `left.len() * right.len()`,常用于「生成所有组合」。
pub fn product_pairs<'a>(left: &[i32], right: &[&'a str]) -> Vec<(i32, &'a str)> {
left.iter()
.copied()
.cartesian_product(right.iter().copied())
.collect()
}
/// 把序列每 `n` 个切成一块,最后一块可能不足 `n` 个。
///
/// `chunks` 返回惰性分组,所以要在持有引用期间把每块收集出来。
pub fn into_chunks(data: &[i32], n: usize) -> Vec<Vec<i32>> {
let mut out = Vec::new();
for chunk in &data.iter().copied().chunks(n) {
out.push(chunk.collect());
}
out
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn zip_three_stops_at_shortest() {
let got = zip_three(
&["小明", "小红", "小刚"],
&[18, 20],
&["北京", "上海", "广州"],
);
// 年龄只有 2 个,所以只产出 2 项。
assert_eq!(got, vec![("小明", 18, "北京"), ("小红", 20, "上海")]);
}
#[test]
fn sorted_ascending_orders_values() {
assert_eq!(sorted_ascending(&[3, 1, 2, 1]), vec![1, 1, 2, 3]);
}
#[test]
fn sorted_by_length_is_stable_by_key() {
let got = sorted_by_length(&["bbb", "a", "cc", "dddd"]);
assert_eq!(got, vec!["a", "cc", "bbb", "dddd"]);
}
#[test]
fn join_with_inserts_separator() {
assert_eq!(join_with(&[1, 2, 3], ", "), "1, 2, 3");
assert_eq!(join_with(&[], ", "), "");
}
#[test]
fn product_pairs_is_full_cross() {
let got = product_pairs(&[1, 2], &["x", "y"]);
assert_eq!(got, vec![(1, "x"), (1, "y"), (2, "x"), (2, "y")]);
}
#[test]
fn into_chunks_keeps_remainder() {
let got = into_chunks(&[1, 2, 3, 4, 5], 2);
assert_eq!(got, vec![vec![1, 2], vec![3, 4], vec![5]]);
}
} examples/47_itertools/src/grouping.rs
//! 分组与去重:用 `itertools` 给迭代器加上「按相邻分组、去重、计数」的能力。
//!
//! 关键点是「扩展 trait(extension trait)」:只要 `use itertools::Itertools;`,
//! 标准库的迭代器就会凭空多出 `.chunk_by()`、`.unique()`、`.counts()` 等方法。
//! 这些方法并不是标准库自带的,而是 `itertools` 通过为所有 `Iterator` 实现
//! `Itertools` trait「补」上去的。
use std::collections::HashMap;
use itertools::Itertools;
/// 把「相邻且相等」的元素压成一组,返回 `(值, 该组长度)` 的列表。
///
/// `chunk_by`(0.14 由旧名 `group_by` 改名而来)只合并**相邻**的相同键,
/// 不会跨越中间不同的元素。例如 `[1, 1, 2, 1]` 会得到三组:`1×2`、`2×1`、`1×1`,
/// 而**不是**把两段 `1` 合并。这点和「先排序再分组」截然不同。
///
/// `chunk_by` 返回的是惰性分组,必须在持有它的引用期间消费,所以这里在
/// `for (key, group) in &iter.chunk_by(...)` 里立即统计长度。
pub fn run_lengths(data: &[i32]) -> Vec<(i32, usize)> {
let mut out = Vec::new();
// 注意 `&...chunk_by(...)`:迭代的是分组结果的引用。
for (key, group) in &data.iter().chunk_by(|&&x| x) {
out.push((key, group.count()));
}
out
}
/// 去掉重复元素,但**保留首次出现的顺序**(不会像 `HashSet` 那样打乱顺序)。
///
/// `unique` 内部用哈希集合记录见过的值,因此元素需要 `Hash + Eq`。
pub fn unique_keep_order(data: &[i32]) -> Vec<i32> {
data.iter().copied().unique().collect()
}
/// 统计每个元素出现的次数,返回一个 `HashMap<值, 次数>`。
///
/// `counts` 会消费迭代器并一次性建好计数表,比手写
/// `for x in it { *map.entry(x).or_insert(0) += 1; }` 更省事。
pub fn word_counts(words: &[&str]) -> HashMap<String, usize> {
words.iter().map(|s| s.to_string()).counts()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn run_lengths_only_merges_adjacent() {
// 末尾的 1 不会和开头的 1 合并,因为它们不相邻。
let got = run_lengths(&[1, 1, 2, 2, 2, 1]);
assert_eq!(got, vec![(1, 2), (2, 3), (1, 1)]);
}
#[test]
fn run_lengths_empty() {
assert_eq!(run_lengths(&[]), vec![]);
}
#[test]
fn unique_preserves_first_seen_order() {
let got = unique_keep_order(&[3, 1, 3, 2, 1, 2]);
assert_eq!(got, vec![3, 1, 2]);
}
#[test]
fn word_counts_tallies_each_key() {
let got = word_counts(&["a", "b", "a", "c", "a", "b"]);
assert_eq!(got.get("a"), Some(&3));
assert_eq!(got.get("b"), Some(&2));
assert_eq!(got.get("c"), Some(&1));
assert_eq!(got.get("z"), None);
}
} examples/47_itertools/Cargo.toml
[package]
name = "rt_47_itertools"
version.workspace = true
edition.workspace = true
publish.workspace = true
[dependencies]
itertools = "0.14"