要点
迭代器适配器描述处理步骤,消费操作才真正取值。iter 借用元素,into_iter 的所有权行为取决于接收者。
目录
核心概念
概念一:集合类型按”内存布局”选型
定义: 标准库集合按数据在内存中的组织方式分为连续型(Vec)、哈希型(HashMap)、树型(BTreeMap)等,选型本质是选择访问模式的代价。
关键特性: – Vec<T>:连续内存、O(1) 下标访问、尾部增删 O(1)——99% 场景的默认选择 – HashMap<K, V>:按键哈希定位,平均 O(1) 查找,但遍历顺序不确定 – BTreeMap<K, V>:有序树,O(log n) 查找,遍历按键排序(需要确定顺序时用它) – String:拥有、可增长且始终有效 UTF-8 的文本类型;可以通过 into_bytes 取得字节向量,但不应依赖其内部表示。不支持 s[i] 下标取字符(字节偏移 ≠ 字符序号)
使用场景: – 收集结果、遍历处理 → Vec – 按键查值、去重计数 → HashMap – 需要按序遍历键 → BTreeMap
概念二:迭代器是”惰性的两段式流水线”
定义: 任何实现 Iterator trait 的类型都产出元素序列;适配器(adapter)返回新的迭代器、什么都不计算,消费器(consumer)才真正驱动计算并产出最终值。
关键特性: – 适配器:map / filter / take / enumerate / zip / skip 等,返回迭代器本身,可链式叠加 – 消费器:collect / sum / count / max / any / fold / for 循环等,触发实际计算 – 惰性求值:只构造适配器链而不调用消费器,不会执行任何一行处理逻辑;配合无限序列(1..)可写”按需取量”的代码 – 零抽象成本:迭代器适配器以静态类型组合,通常可优化为与直接循环接近的代码;是否内联及最终性能仍取决于具体代码、编译选项和测量结果
使用场景: – 场景1:过滤-变换-收集一条龙(filter → map → collect) – 场景2:从无限/大数据源按需取前 N 条(take) – 场景3:聚合统计(sum / count / max / fold)
概念三:三种遍历方式的所有权语义
定义: iter() 借用、iter_mut() 可变借用、into_iter() 消费——三种方法对应三种所有权转移策略。
关键特性: – for x in &v ≡ v.iter():拿到 &T,集合保留 – for x in &mut v ≡ v.iter_mut():拿到 &mut T,可原地修改 – for x in v ≡ v.into_iter():拿到 T,集合被消费,之后不可再用
所有权细节回顾见 02-所有权与借用。
实践指南
步骤一:用 Vec 完成增删改查
目标: 建立对 Vec API 与所有权行为的肌肉记忆。
操作指南: 1. 用 vec![] 宏初始化,push / pop / remove / insert 增删 2. 下标 v[0] 越界会 panic,安全读取用 v.get(i) 拿 Option 3. 遍历时优先 &v,确认不再需要集合本身时才 v 直接消费
验证方法: cargo run 观察输出;故意越界触发 panic 观察报错信息。
步骤二:用 HashMap 处理”键值问题”
目标: 掌握 entry 惯用法,这是 HashMap 最有 Rust 特色的 API。
操作指南: 1. get 返回 Option,用 match 或 if let 处理 2. “不存在才插入、存在则修改”一律用 entry(k).or_insert(v) 3. 注意:HashMap 键必须实现 Eq + Hash;自定义类型加 #[derive(PartialEq, Eq, Hash)]
验证方法: 运行下方代码示例二,确认词频统计输出。
步骤三:把手写循环重写为适配器链
目标: 体验”声明式流水线”的写法。
操作指南: 1. 找一段”循环 + if 判断 + 逐个 push”的代码 2. 拆成三段:数据源 → 适配器(filter/map)→ 消费器(collect/sum) 3. 编译器若报”类型标注不足”,在 let 右侧补目标类型或用 turbofish ::<>
验证方法: 两种写法输出一致,且适配器链版本无临时 Vec 中间变量。
代码示例
示例一:Vec 的增删改查与所有权
// 块1:Vec 增删改查
fn main() {
// 创建:vec! 宏初始化
let mut v: Vec<i32> = vec![1, 2, 3];
// 增
v.push(4);
// 删:移除下标 0,返回被移除的元素
let removed = v.remove(0);
// 改:下标越界会 panic,需要安全访问时用 get
v[0] = 10;
// 查:get 返回 Option<&i32>,越界安全
let third = v.get(2);
println!("removed = {removed}, v = {v:?}, third = {:?}", third);
// 遍历:for x in &v 只借用,v 之后还能用
for x in &v {
print!("{x} ");
}
println!();
// for x in v 会消费(move)v,此后 v 不可再用
let consumed: Vec<String> = v.iter().map(|x| x.to_string()).collect();
println!("{consumed:?}");
}
关键点解析: – remove(0) 返回被移除的元素(所有权交还给你),get 返回借用(Option<&i32>) – v[0] = 10 与 v.get(0) 的区别是”panic vs Option”——库代码里永远倾向后者 – consumed 收集时是 map 产出新 String,原 v 未被消费
示例二:HashMap 与 entry 惯用法
// 块2:HashMap 基础与 entry 惯用法
use std::collections::HashMap;
fn main() {
let mut scores: HashMap<String, i32> = HashMap::new();
scores.insert(String::from("Alice"), 92);
scores.insert(String::from("Bob"), 85);
// 查询:返回 Option,强制调用方处理"键不存在"
match scores.get("Alice") {
Some(s) => println!("Alice: {s}"),
None => println!("查无此人"),
}
// entry:不存在才插入,存在则原值保留
scores.entry(String::from("Carol")).or_insert(0);
*scores.entry(String::from("Bob")).or_insert(0) += 5;
println!("{scores:?}");
// 词频统计:entry 的经典场景
let text = "rust is fast rust is safe";
let mut freq: HashMap<&str, i32> = HashMap::new();
for word in text.split_whitespace() {
*freq.entry(word).or_insert(0) += 1;
}
println!("{freq:?}");
}
关键点解析: – entry 一次哈希定位完成”查 + 插 + 改”,比先 contains_key 再操作少一次查找 – or_insert 返回 &mut V,* 解引用后可直接累加 – 词频里的键是 &str(借用 text),数据不必复制进 HashMap
示例三:适配器链替换手写循环
// 块3:适配器链
fn main() {
let nums = vec![1, 2, 3, 4, 5, 6];
// 适配器链:filter -> map -> sum
// sum 是消费器,驱动整条链执行并产出结果
let total: i32 = nums
.iter()
.filter(|&&x| x % 2 == 0) // 只留偶数
.map(|x| x * 10) // 每个乘 10
.sum();
println!("{total}"); // 20 + 40 + 60 = 120
// collect:把迭代器收回到集合
let names = vec![" alice ", " bob", "carol "];
let cleaned: Vec<String> = names
.iter()
.map(|s| s.trim().to_uppercase())
.collect();
println!("{cleaned:?}");
// collect 也能产出 HashMap / String 等任何实现了 FromIterator 的类型
let by_len: std::collections::HashMap<&str, usize> =
names.iter().map(|s| (s.trim(), s.trim().len())).collect();
println!("{by_len:?}");
}
关键点解析: – collect 的目标类型靠 let 标注或 turbofish 指定,否则编译器无法推断 – filter 闭包参数是 &&T(迭代器产出 &T,filter 再借一层),模式 &&x 直接解到值
示例四:惰性求值与消费器速览
// 块4:惰性求值与消费器
fn main() {
// 惰性求值:不写消费器,适配器什么也不做
let _no_op = [1, 2, 3].iter().map(|x| x * 2); // 仅构造,零计算
// 无限序列 + take:消费器要多少,才算多少
let squares: Vec<i64> = (1..).map(|x| x * x).take(5).collect();
println!("{squares:?}"); // [1, 4, 9, 16, 25]
// enumerate:携带下标
let names = ["a", "b", "c"];
for (i, n) in names.iter().enumerate() {
println!("{i}: {n}");
}
// 常用消费器速览
let v = vec![3, 1, 4, 1, 5];
let max = v.iter().max();
let count_even = v.iter().filter(|&&x| x % 2 == 0).count();
let any_negative = v.iter().any(|&x| x < 0);
let joined: String = v
.iter()
.map(|x| x.to_string())
.collect::<Vec<_>>()
.join("-");
println!("{max:?} {count_even} {any_negative} {joined}");
}
关键点解析: – (1..) 是上界不设限的 RangeFrom,只有 take(5) 这样的消费器能终止它 – 惰性意味着”适配器链 = 计划,消费器 = 执行”,这与函数式语言(Haskell/Scala)的流一致 – any / all 是短路消费器:找到答案立即停止
示例五:适配器 + 单元测试
// 块5:迭代器 + 单元测试
fn normalize(text: &str) -> Vec<String> {
text.split_whitespace()
.map(|w| w.trim_matches(|c: char| !c.is_alphanumeric()).to_lowercase())
.filter(|w| !w.is_empty())
.collect()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn 提取并小写化() {
assert_eq!(normalize("Rust, is FAST!"), vec!["rust", "is", "fast"]);
}
#[test]
fn 纯标点被过滤() {
assert!(normalize("... !!!").is_empty());
}
}
测试工程化的完整介绍见 10-Cargo 工程化与单元测试。
最佳实践
根据是否保留集合选择借用遍历或消费遍历,两者成本取决于元素与后续使用,并非 iter 永远更便宜。迭代器适配器通常按需执行,只有实际消费才处理元素;也可以返回迭代器让调用方稍后消费。
HashMap 的 entry 适合“存在就修改、不存在就插入”,单纯查存在仍可用 contains_key。字符串按字节和 Unicode 标量值遍历含义不同,不能用整数下标当字符定位。需要稳定输出时显式排序或选有序结构,不依赖哈希遍历顺序。
常见问题
Q1: iter()、into_iter()、iter_mut() 到底选哪个?
A: 看”之后还要不要用这个集合”。还要用 → iter();要原地改 → iter_mut();集合就是为这次遍历而生的 → into_iter()(顺便转移元素所有权,如把 Vec<String> 拆成一个个 String)。借用与移动的完整规则见 02-所有权与借用。
Q2: 为什么 filter 的闭包参数是 &&T?
A: v.iter() 产出 &T,filter 的谓词接收的是”对元素的引用”再借一层,所以是 &&T。用模式 |&&x| 一次解两层即可;记不住时先写 |x| 让编译器报错提示类型,再补解引用层数。
Q3: collect 总报类型推断失败怎么办?
A: 两种补救:① 在 let 左侧写明类型 let v: Vec<String> = ...collect();;② turbofish 就地指定 collect::<Vec<String>>()。本质是 collect 太通用(任何 FromIterator 类型都是候选),必须给编译器一个目标。
相关资源
延伸阅读
- 官方文档: std::vec::Vec – Vec 全量 API
- 官方文档: std::iter – 迭代器 trait 与全部适配器/消费器
- 书籍: 《The Rust Programming Language》ch.8 / ch.13 – 集合与迭代器闭包的官方教程
工具资源
- 开发工具: Rust Playground – 快速验证适配器链行为
- 在线平台: Rust By Example – 标准库逐例演示
练习与实践
练习一:词频统计升级
目标: 综合 HashMap + 迭代器。
任务要求: 1. 输入一段英文文本,输出按出现次数降序的前 5 个单词 2. 只用迭代器适配器链(entry 统计 + sort_by / sort_unstable_by + take) 3. 为核心函数写至少 2 个单元测试
评估标准: 不出现手写下标循环;测试覆盖”并列次数”与”空输入”。
练习二:惰性流水线
目标: 体会按需计算。
挑战任务: – 用 (1..) 生成前 10 个质数(filter + is_prime 闭包 + take) – 挑战:把同一适配器链分别 take(10) 与 take(1000),确认无需改写中间逻辑
提示: 质数判断用 2..=(n 的平方根) 的 all 消费器。
知识图谱
前置知识
graph TD
A[所有权与借用] --> B[闭包与 trait 初步]
B --> C[集合与迭代器]
后续学习
graph LR
C[集合与迭代器] --> D[生命周期标注]
C --> E[智能指针]
C --> F[Tokio 异步 Stream]
文档交叉引用
相关文档
- 📄 所有权与借用: 三种遍历方式的所有权根源
- 📄 结构体、枚举与模式匹配:
match处理get返回的Option - 📄 trait 与泛型: Iterator 本身就是一个 trait
- 📄 Cargo 工程化与单元测试: 本篇测试代码的组织方式
参考章节
- 📖 所有权字典: 借用细则的单一事实来源
- 📖 模块 README: 技术基线与模块路径图
总结
核心要点回顾
- 集合选型看访问模式: Vec 是默认,按键查值上 HashMap,要有序用 BTreeMap
- 迭代器两段式: 适配器搭流水线(惰性、零成本),消费器触发执行
- entry 是 HashMap 惯用法: 一次哈希完成”查缺补漏改”
学习成果检查
- [ ] 能说清
iter/into_iter/iter_mut的所有权差异? - [ ] 能把一段手写循环重写为适配器链?
- [ ] 能用
entry完成词频统计? - [ ] 能解释”惰性求值”并写出无限序列取前 N 项?
贡献与反馈
发现本文档有改进空间?欢迎在 Issues 报告问题、提出改进建议,或直接提交 PR 完善内容。
文档版本: v1.0.0最后更新: 2026年9月维护团队: Hello Team
💡 学习建议: 每个示例亲手敲一遍并
cargo run,迭代器的类型推导错误是最好的老师。🎯 下一步: 返回值里的引用如何保证不悬垂?继续学习 07-生命周期标注。