趣味学RUST基础篇(函数式编程迭代器)

发布于:2025-09-10 ⋅ 阅读:(20) ⋅ 点赞:(0)

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")
                },
            ]
        );
    }
}

你对实习生说:

  1. “把这堆鞋子拿走(into_iter)。”
  2. “只留下鞋码是 shoe_size 的(filter)。”
  3. “把留下的鞋子装个新盒子给我(collect)。”

这里的 filter 用了一个 闭包(closure),它“偷看”了你定义的 shoe_size 变量,就像实习生记住了你的指令。


迭代器的“三大纪律”

  1. 它是懒的:不调用 forsumcollect 这些“消费”方法,它绝不干活。
  2. 它分两种
    • 消费适配器:像 sum,用完就“吃掉”迭代器。
    • 迭代器适配器:像 mapfilter,它们“改造”迭代器,但不消费,需要你再用 collect 来“变现”。
  3. 它很灵活:你可以把多个适配器串起来

Rust 进化论:从“搬砖”到“念咒”

还记得我们之前写的那个“搜索文件”小工具吗?它能帮你在一个文本文件里找关键词,比如:“哪里提到了‘猫’?”但当时的代码,就像一个原始人用石头和木棍打猎——能用,但不够优雅。

今天,我们要用 “迭代器魔法” 给它来一次大升级,让它从“搬砖”变成“念咒”!


阶段一:原始人搬砖 —— clonefor 循环

在旧版本里,我们的 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, // 拿到第二个,就是 query
            None => 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 社区的共识优先使用迭代器

因为它:

  1. 更少的可变状态 → 更少的 bug。
  2. 更易并行化 → 未来可以轻松改成多线程搜索。
  3. 更接近“做什么”,而不是“怎么做” → 代码更易维护。

从“搬砖工”到“魔法师”

阶段 工具 代码风格 比喻
原始人 clone, for 搬砖、复印 用石头打猎
未来战士 迭代器 念咒、魔法 用激光枪

记住这三句咒语

  1. args.next() —— “下一个!”
  2. .filter(|x| ...) —— “只留下符合条件的!”
  3. .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 代码吧!


网站公告

今日签到

点亮在社区的每一天
去签到