简介
显式使用 for 循环通常是处理集合最清晰的方式。Rust 还提供了迭代器:它们每次生成一个元素,并且可以组合成易读的数据处理流水线。
你将保留一个可运行的循环作为基准,使用 iter 和 sum 得到相同的总和,编写一个小型闭包,使用 map 转换读数,使用 filter 排除无效读数,最后将这些操作组合起来。所有修改都在准备好的 Cargo 项目中完成,因此你可以专注于学习新的 Rust 概念。
使用迭代器复现循环总和
在本步骤中,你将把准备好的 for 循环与计算相同总和的迭代器进行比较。
项目位于 /home/labex/project/reading-pipeline,程序文件是 src/main.rs。准备好的 vec![12, 18, -1, 24, 30] 使用 vec! 宏,以简洁的方式创建包含这些值的向量;它与前一个 Vec 实验中先创建空向量、再逐个添加值的做法目的相同。现有循环通过 &readings 借用该向量,依次访问每个 &i32,并使用 *reading 获取引用所指向的整数。
进入项目并运行基准程序:
cd /home/labex/project/reading-pipeline
cargo run --quiet
Loop total: 83
总和包含每个读数,其中也包括 -1。现在打开源文件:
nano src/main.rs
将 Step 1 注释替换为:
let iterator_total: i32 = readings.iter().sum();
println!("Iterator total: {iterator_total}");
readings.iter() 创建一个迭代器,借用向量中的每个元素。它不会移除或消耗该向量。sum() 会不断累加生成的元素;Rust 标准库知道如何对借用的整数求和。类型标注 : i32 指定了 sum 应该生成的结果类型。
使用 Ctrl+O 保存,按 Enter 确认,然后使用 Ctrl+X 退出。运行程序前先检查代码:
cargo check
cargo run --quiet
Loop total: 83
Iterator total: 83
两行结果相同,说明循环和迭代器描述的是同一个计算过程。由于 iter() 只借用了向量,后续步骤仍然可以使用该向量。
使用闭包和 Map 转换读数
在本步骤中,你将定义一个闭包,并使用 map 将它应用于每个读数。
闭包是一种小型的匿名函数,可以存储在变量中,也可以直接传递给迭代器适配器。形式 |parameter: Type| expression 将参数放在两条竖线之间,并返回表达式的值。这里的校准闭包会为读数加上偏移量 2。
打开源文件:
nano src/main.rs
将 Step 2 注释替换为:
let add_offset = |reading: i32| reading + 2;
let adjusted: Vec<i32> = readings.iter().map(|reading| add_offset(*reading)).collect();
println!("Adjusted all: {adjusted:?}");
map 会对迭代器中的每个元素调用一次闭包,并生成闭包返回的值。类型流转过程是:iter() 生成 &i32 → *reading 复制出 i32 → add_offset 生成调整后的 i32 → collect() 收集为 Vec<i32> 的元素。:? 格式说明符会以易读的调试格式打印向量。
保存并退出,然后检查并运行程序:
cargo check
cargo run --quiet
新增的最后一行应为:
Adjusted all: [14, 20, 1, 26, 32]
即使无效读数 -1 也被 map 转换成了 1;map 负责转换元素,但不会决定哪些元素应该保留。
使用 Filter 仅保留有效读数
在本步骤中,你将使用 filter 保留非负读数,同时保持原始向量不变。
迭代器的 filter 适配器会为每个候选元素调用谓词闭包。谓词返回 true 时保留元素,返回 false 时排除元素。打开源文件:
nano src/main.rs
将 Step 3 注释替换为:
let valid: Vec<i32> = readings
.iter()
.copied()
.filter(|reading| *reading >= 0)
.collect();
println!("Valid only: {valid:?}");
方法链可以在点号后换行,并在缩进后的行中继续。iter() 借用向量,copied() 将每个借用的 &i32 元素转换为 i32;由于整数实现了 Copy,这个操作开销很小。然后,filter 谓词会接收每个 i32 候选元素的临时引用,从而在不消耗元素的情况下检查它,因此比较表达式使用了 *reading。类型流转过程是:&i32 → 复制出的 i32 → 保留的 i32 → Vec<i32>。
保存并退出,然后运行程序:
cargo run --quiet
最后一行应为:
Valid only: [12, 18, 24, 30]
负数值已被排除,保留的值仍然按照原始顺序排列。
组合完整的数据处理流水线
在本步骤中,你将依次执行过滤和映射,收集处理后的读数,并计算这些读数的总和。
迭代器适配器是惰性的:filter 和 map 只描述要执行的工作,只有当消耗操作请求结果时,元素才会流经这些适配器。collect 和 sum 都是消耗操作。它们会消耗迭代器流水线,但不会消耗被借用的源向量。
打开源文件:
nano src/main.rs
将 Step 4 注释替换为:
let valid_adjusted: Vec<i32> = readings
.iter()
.copied()
.filter(|reading| *reading >= 0)
.map(add_offset)
.collect();
let valid_total: i32 = valid_adjusted.iter().sum();
println!("Valid adjusted: {valid_adjusted:?}");
println!("Valid adjusted total: {valid_total}");
该流水线首先排除 -1,然后由 map(add_offset) 将其余的 i32 传递给前面定义的闭包。如果闭包的参数类型和返回类型符合适配器的要求,就可以直接提供闭包名称。第二个迭代器借用收集后的向量,并计算其总和。
保存并退出,然后检查并运行程序:
cargo check
cargo run --quiet
最后两行应为:
Valid adjusted: [14, 20, 26, 32]
Valid adjusted total: 92
操作顺序很重要:先过滤再映射,可以避免无效读数被转换成看似有效的正数。原始 readings 向量始终只被借用,因此之后仍然可以继续使用。
总结
你比较了显式循环和迭代器,编写并复用了闭包,使用 map 转换值,使用 filter 选择值,收集了一个新向量,并使用 sum 计算总和。你还通过在每条流水线开头使用借用迭代器,保留了源向量。


