趣味学RUST基础篇(函数式编程迭代器)
Rust 迭代器:一个“懒人”如何高效打工
想象一下,你是一个程序员,刚入职一家叫 “Rust公司” 的高科技企业。你的任务是处理一堆数据,比如:一个装着数字 [1, 2, 3]
的盒子。
第一幕:创建一个“打工人”——迭代器
你不能直接把盒子拆开一个个数,太原始了!Rust 公司给你配了一个聪明的“打工人”——迭代器(Iterator)。
let v1 = vec![1, 2, 3];
let v1_iter = v1.iter(); // 召唤“打工人”
这就像你对 HR 说:“嘿,给我派个实习生,让他帮我数数这个盒子里有啥。”
重点来了:这个实习生超级“懒”!
你刚叫他,他只是站在那儿,啥也不干。这就是 “惰性(lazy)” ——不干活,除非你下命令。
第二幕:让他干活——for
循环
你终于发话了:“开始干活,把每个数都打印出来!”
for val in v1_iter {println!("Got: {val}");
}
实习生立刻动起来:
- 第一天:拿出
1
,打印。 - 第二天:拿出
2
,打印。 - 第三天:拿出
3
,打印。 - 第四天:盒子空了,他交出
None
,下班!
内部发生了啥?
实习生其实有个 next()
方法,每次调用就吐出一个值:
#[test]fn iterator_demonstration() {let v1 = vec![1, 2, 3];let mut v1_iter = v1.iter();assert_eq!(v1_iter.next(), Some(&1));assert_eq!(v1_iter.next(), Some(&2));assert_eq!(v1_iter.next(), Some(&3));assert_eq!(v1_iter.next(), None);}
小贴士:要让实习生动起来,得把他变成“可变”的(
mut
),因为他得记住自己数到哪儿了。
第三幕:实习生也能“算工资”——消费适配器
你又说:“别光打印了,把所有数加起来,算个总和!”
#[test]fn iterator_sum() {let v1 = vec![1, 2, 3];let v1_iter = v1.iter();let total: i32 = v1_iter.sum();assert_eq!(total, 6);}
实习生立刻把盒子拿过来,从头到尾数一遍,累加,最后交出 6
。
但注意!他把盒子拿走后就不还你了!
这就是 “消费适配器(consuming adaptor) ——用完就“吃掉”迭代器,你不能再用它了。
第四幕:实习生变身“改造大师”——迭代器适配器
现在你不想让他算总数,而是想让每个数都 加 1。你对他说:
v1.iter().map(|x| x + 1);
实习生听完,还是站着不动!
为啥?因为他又“懒”了。他只是说:“哦,我知道了,如果让我干,我就把每个数加1。”
但你不让他干活,他就不动。
警告:Rust 编译器会提醒你:“你造了个改造大师,但没用他,浪费了!”
怎么让他干活?得让他“变现”!比如,让他把改造后的结果 收集 起来:
//main.rs
let v1: Vec<i32> = vec![1, 2, 3];let v2: Vec<_> = v1.iter().map(|x| x + 1).collect();
assert_eq!(v2, vec![2, 3, 4]);
collect()
就像说:“把你的成果打包成一个新盒子交给我!”
这叫 “迭代器适配器(iterator adaptor) ——它不消耗原数据,而是生成一个新“打工人”来做改造。
第五幕:实习生还会“筛选”——filter
你有一堆鞋子,想找出所有 10码 的。
#[derive(PartialEq, Debug)]
struct Shoe {size: u32,style: String,
}fn shoes_in_size(shoes: Vec<Shoe>, shoe_size: u32) -> Vec<Shoe> {shoes.into_iter().filter(|s| s.size == shoe_size).collect()
}#[cfg(test)]
mod tests {use super::*;#[test]fn filters_by_size() {let shoes = vec![Shoe {size: 10,style: String::from("sneaker"),},Shoe {size: 13,style: String::from("sandal"),},Shoe {size: 10,style: String::from("boot"),},];let in_my_size = shoes_in_size(shoes, 10);assert_eq!(in_my_size,vec![Shoe {size: 10,style: String::from("sneaker")},Shoe {size: 10,style: String::from("boot")},]);}
}
你对实习生说:
- “把这堆鞋子拿走(
into_iter
)。” - “只留下鞋码是
shoe_size
的(filter
)。” - “把留下的鞋子装个新盒子给我(
collect
)。”
这里的 filter
用了一个 闭包(closure),它“偷看”了你定义的 shoe_size
变量,就像实习生记住了你的指令。
迭代器的“三大纪律”
- 它是懒的:不调用
for
、sum
、collect
这些“消费”方法,它绝不干活。 - 它分两种:
- 消费适配器:像
sum
,用完就“吃掉”迭代器。 - 迭代器适配器:像
map
、filter
,它们“改造”迭代器,但不消费,需要你再用collect
来“变现”。
- 消费适配器:像
- 它很灵活:你可以把多个适配器串起来
Rust 进化论:从“搬砖”到“念咒”
还记得我们之前写的那个“搜索文件”小工具吗?它能帮你在一个文本文件里找关键词,比如:“哪里提到了‘猫’?”但当时的代码,就像一个原始人用石头和木棍打猎——能用,但不够优雅。
今天,我们要用 “迭代器魔法” 给它来一次大升级,让它从“搬砖”变成“念咒”!
阶段一:原始人搬砖 —— clone
和 for
循环
在旧版本里,我们的 Config::new
函数是这样工作的:
let query = args[1].clone();
let file_path = args[2].clone();
这就像你对助手说:
“去,把第一个参数拿过来,复印一份给我;再把第二个参数拿过来,也复印一份!”
为什么要“复印”?因为助手(函数)不能直接拿走你的东西,他得自己留一份。
问题来了:复印(clone
)很浪费时间!我们能不能直接把原件给他?
阶段二:念咒语 —— 用迭代器“隔空取物”
好消息!Rust 的 env::args()
函数其实返回的不是一个“参数列表”,而是一个 “参数生成器” ——也就是迭代器!
它就像一个会自动吐出参数的魔法盒子,你只要说“下一个!”,它就吐一个。
第一步:把“复印机”扔了,直接传“魔法盒子”
我们不再把参数收集到 Vec
里再传 slice,而是直接把“魔法盒子”(迭代器)交给 Config::
new:
// 旧的:先收集,再传 slice
let args: Vec<String> = env::args().collect();
let config = Config::new(&args)...;// 新的:直接传“魔法盒子”!
let config = Config::new(env::args())...;
看,多干净!连 collect()
都省了。
第二步:改写 new 函数,学会“念咒”
现在 new 函数要改了,它不再接受一个“复印好的列表”,而是接受一个“会吐参数的盒子”:
impl Config {pub fn new(mut args: impl Iterator<Item = String>, // 接收任何能吐出 String 的“盒子”) -> Result<Config, &'static str> {args.next(); // 第一个是程序名,扔掉!(念:下一个!)let query = match args.next() {Some(arg) => arg, // 拿到第二个,就是 queryNone => return Err("没给关键词!"),};let file_path = match args.next() {Some(arg) => arg, // 拿到第三个,就是文件路径None => return Err("没给文件路径!"),};// 其他逻辑不变...Ok(Config { query, file_path, ignore_case })}
}
关键点:
- 我们用
args.next()
来“念咒”,让盒子吐出下一个参数。 - 因为是直接“拿走”参数,所以 不需要
clone
!省时省力。 - 如果盒子空了(
None
),就说明参数不够,报错!
进化成功!从“复印”到“隔空取物”,代码更高效了!
🔍 阶段三:搜索函数也来“念咒”!
再看搜索函数 search
,旧版本是这样:
pub fn search(query: &str, contents: &str) -> Vec<&str> {let mut results = Vec::new(); // 准备一个空篮子for line in contents.lines() { // 一行行看if line.contains(query) { // 如果这行有关键词results.push(line); // 放进篮子}}results // 返回篮子
}
这就像一个工人,一行一行地检查,符合条件就放进篮子里。虽然能干,但太“机械”了。
用迭代器“念咒”:
pub fn search(query: &str, contents: &str) -> Vec<&str> {contents.lines() // 把文本拆成“行流”.filter(|line| line.contains(query)) // 念咒:只留下包含关键词的行.collect() // 把结果“打包”成 vector
}
一句话搞定!就像对魔法阵下令:
“把所有行过滤一遍,只留下包含‘猫’的,然后打包给我!”
优点:
- 代码更短:从 7 行变成 4 行。
- 意图更清晰:一眼看出“过滤 + 收集”的逻辑。
- 没有可变变量:
results
篮子没了,代码更“函数式”,更安全。 - 性能可能更好:Rust 编译器对迭代器优化得非常好。
循环 vs 迭代器:选哪个?
你可能会问:“for 循环”和“迭代器”哪个更好?
风格 | 优点 | 缺点 | 适合谁 |
---|---|---|---|
for 循环 | 直观,像“手把手教” | 代码长,容易出错 | 初学者 |
迭代器 | 简洁,意图明确,更安全 | 初学时有点抽象 | Rust 老手 |
Rust 社区的共识:优先使用迭代器!
因为它:
- 更少的可变状态 → 更少的 bug。
- 更易并行化 → 未来可以轻松改成多线程搜索。
- 更接近“做什么”,而不是“怎么做” → 代码更易维护。
从“搬砖工”到“魔法师”
阶段 | 工具 | 代码风格 | 比喻 |
---|---|---|---|
原始人 | clone , for | 搬砖、复印 | 用石头打猎 |
未来战士 | 迭代器 | 念咒、魔法 | 用激光枪 |
记住这三句咒语:
args.next()
—— “下一个!”.filter(|x| ...)
—— “只留下符合条件的!”.collect()
—— “打包带走!”
从此,你不再是那个手动 for i in 0..len
的“搬砖工”,而是一个能用一行代码搞定复杂逻辑的 Rust 魔法师!
Rust 性能大对决:手动挡 vs 自动挡,谁更快?
我们可以把循环和迭代器比作手动挡和自动挡,想象一下,你正在看一场赛车比赛。
赛道左边是一辆纯手动挡赛车——它代表我们手写的 for
循环。
赛道右边是一辆智能自动挡赛车——它代表我们用迭代器写的代码。
它们的任务是:在一本厚厚的《福尔摩斯探案集》里,找出所有“the”这个单词,看谁完成得更快!
第一回合:正式比赛开始!
我们把书加载进内存,两辆车同时发车!
成绩出来了!
- 手动挡(for 循环):19,620,300 纳秒(约 0.02 秒)
- 自动挡(迭代器):19,234,900 纳秒(约 0.019 秒)
什么?!自动挡居然还快了一丢丢?!
这就像你本以为手动换挡能更精准控制,结果发现自动变速箱的 AI 换挡比你还快还顺!
结论:迭代器 ≠ 慢!它和手写循环一样快,甚至更快!
为什么自动挡这么猛?
因为 Rust 的编译器,是个超级赛车调校大师!
它看到你写的“高级”代码,比如:
.filter(|line| line.contains(query))
.collect()
它不会傻乎乎地照着代码一行行翻译成机器指令。
相反,它会说:“哦,用户想过滤再收集?我懂了,我给你优化成最高效的汇编代码!”
这就像:
- 你对导航说:“带我去最近的咖啡馆。”
- 导航不会真的“找最近”,而是直接算出最优路线,带你飞过去。
Rust 编译器就是这么聪明!
第二回合:更复杂的赛道 —— 音频解码器
这次的赛道更难了!我们要解码一段音频,计算一个叫 prediction
的值。
手动挡车手会怎么写?
for i in 12..buffer.len() {let mut sum = 0;for j in 0..12 {sum += coefficients[j] * buffer[i - 12 + j] as i64;}let prediction = sum >> qlp_shift;// ... 后续操作
}
嵌套循环,手动索引,容易出错,看得人头大。
自动挡车手(Rust 迭代器) 怎么写?
let prediction = coefficients.iter().zip(&buffer[i - 12..i]).map(|(&c, &s)| c * s as i64).sum::<i64>() >> qlp_shift;
一句话,清晰明了:“把系数和数据配对,相乘,求和,再右移。”
结果呢?
在编写这本书的时候,这两段代码被编译成了完全相同的汇编代码!
编译器看到 coefficients.iter()
只有 12 个元素,直接把循环展开(unroll)——也就是把 12 次循环变成 12 行重复代码,彻底干掉了“循环计数”的开销!
所有数据都放进寄存器(CPU 的超级高速缓存),访问飞快!
没有数组越界检查!没有多余的跳转!
这就是 Rust 的“零成本抽象”(Zero-Cost Abstractions)!
什么是“零成本抽象”?
简单说就是:
你用高级语法写的代码,Rust 编译器能把它变成和你手写汇编一样高效的机器码。
就像:
- 你用“自动驾驶”模式开车,结果发现它比你手动开还省油、还安全。
- 你用“美颜滤镜”拍照,结果发现画质和原图一模一样,只是更好看了。
Rust 说:
“你不用的,不收你钱(不引入开销);你用了的,我给你做到极致(不可能手写更好)。”
这正是 C++ 之父 Bjarne Stroustrup 说的 “零开销(zero-overhead)” 原则!
放心大胆用迭代器!
项目 | 手写 for 循环 | Rust 迭代器 |
---|---|---|
性能 | 快 | 一样快,甚至更快 |
可读性 | 难懂,易错 | 清晰,意图明确 |
安全性 | 容易越界 | 编译器帮你检查 |
维护性 | 难改 | 易重构 |
所以,别再担心“用迭代器会不会变慢”了!
Rust 的编译器是你的超级外挂。你只管用优雅、安全的高级语法写代码,剩下的性能优化,交给它!
现在,放下对性能的担忧,大胆地使用闭包和迭代器,写出既漂亮又飞快的 Rust 代码吧!