如何用Rust优雅地写迭代?

277 阅读5分钟

不吐不快:最近在力扣的过程中,经常会遇到for循环、while循环,各种循环的过程,然后边界判断一箩筐,状态转换一箩筐,最后还没有访问越界。小心翼翼地打开力扣的题解,看到他们优雅地用迭代器各种求解,“艹,这么牛逼,玛德。”。我实在是受够了自己羸弱的代码能力,决定把迭代交给Rust官方的迭代器。话不多说,我们也走起!

迭代器,顾名思义,就是帮助我们进行迭代。我们可以先想这么一个事情,假设现在需要从1打印到100,我们正常做法是什么?

for i in (0..100) {
    print!("{}", i);
}

如果你会这样写,说明你已经掌握90%迭代器的使用方法了。但可惜的是,我们经常会各种访问越界,为什么呢?因为我们通常在迭代的过程中,是需要对集合(例如数组、向量、链表等)进行访问和操作的。

这么一访问,坏事了,天知道会有什么问题。

为了防止我们胡乱地访问,Rust官方提供了一种声明式的方式来遍历序列,其核心思想是数据处理和数据本身分离

什么意思呢,还是上边的代码,我们优化一下。

(0..100).for_each(|x|print!("{}", x));

我们能够明显地看到,遍历这一繁琐的操作就这样被我们一笔带过了,我们会更加专注于功能上的实现,也就是打印输出。

到现在还是懵懵懂懂的对不对?

别着急,还剩10%,如果我们乍一看各种教程方法论,里边各种规则会让我们十分头疼,我会给每一个规则都举出一些例子,尽量通俗易懂一些。

先创建一个迭代器

我现在定义一个数组let nums = vec![1,2,3,4,5];,我现在假设有这么一个需求,我需要对每一个元素都进行平方,并且返回一个新的数组,该怎么办呢?

先看传统方法:

let mut squared = Vec::new();

for num in nums {
    squared.push(num * num);
}

很棒,优美的写法,但是我们发现没有,这实际上是一种一对一的映射,1变成1,2变成4,以此类推,刚好Rust给我们提供了这种映射方式,请看:

let squared: Vec<i32> = nums.iter().map(|x| x*x).collect();

// 也可以写成
let squared: Vec<i32> = nums.iter()
    .map(|x| x*x)
    .collect();

这iter是啥呀,map、collect又是啥呀?

实际上,.iter(), .iter_mut(), .into_iter(),这三种方法都可以用来创建迭代器。

let vec = vec![1,2,3];
let iter = vec.iter();
let iter1 = vec.into_iter();
let iter2 = vec.iter_mut();

into_iter,它被叫做消费迭代器,会获取集合的所有权,集合本身会被消费掉。这意味着在使用into_iter之后,原集合不能再被访问,因为它的所有权已经被转移给了迭代器。

fn process_lines(lines: Vec<String>) {
    for line in lines.into_iter() {
        // 处理每一行,获取所有权
        println!("Processing line: {}", line);
        // 可以在这里对 line 进行修改或消耗操作
        // line 在每次迭代后都会被丢弃
    }
    // 此时 lines 向量已经不能被访问,因为它已经被消费掉
}

fn main() {
    let file_lines = vec!["Line 1".to_string(), "Line 2".to_string()];
    process_lines(file_lines);
    // 下面的代码会报错,因为 file_lines 已经被移动
    // println!("After processing: {:?}", file_lines);
}

iter是一个只读的迭代器,它不会获取集合的所有权,只是借用集合的引用来进行迭代。这样可以在不改变集合内容的情况下,安全地读取每一个元素。注意,这里提到是读取,实际上也是不能修改的。 我们可以看下边这个例子:

fn main() {
    let nums = vec![1, 2, 3, 4, 5];

    // 使用 |&x| 提取值
    let squares: Vec<i32> = nums.iter().map(|&x| x * x).collect();
    println!("Squares with |&x|: {:?}", squares);

    // 不使用 |&x|,手动解引用
    let squares_ref: Vec<i32> = nums.iter().map(|x| *x * *x).collect();
    println!("Squares without |&x|: {:?}", squares_ref);
}

nums.iter()会返回一个迭代器,每次迭代返回一个&i32的值,而在map的过程中,|&x|在闭包参数中进行模式匹配,自动解引用x,那么x就是i32类型的值,因此x*x是直接对值进行操作。

而|x|接受一个&i32类型的引用,x手动解引用x,获取i32类型的值,因此x同样是对值进行操作。

最后iter_mut()是生成一个可变迭代器,允许你在迭代过程中修改集合中的元素,它和iter类似,但是对元素提供了可以修改的访问权限,例如:

fn scale_values(vec: &mut Vec<f64>, factor: f64) {
    for num in vec.iter_mut() {
        *num *= factor;
    }
}

fn main() {
    let mut values = vec![1.0, 2.0, 3.0];
    scale_values(&mut values, 2.5);
    // values 可以在这里继续被访问和修改,因为它只是被借用给了 scale_values 函数
    println!("Scaled values: {:?}", values);
}

实际上会对vec中的所有的值进行修改,但是又没有获取到它的所有权。

迭代器的大方法

之前我们提到的map就是迭代器中比较常见的方法,下面我们分类介绍一些比较常见好用的方法,同时也把我们在刷题过程中遇到的迭代(动态规划、双指针)等问题应用起来。

未完待续......

Rust 迭代器 | 菜鸟教程 明天接着写,不着急一天写完,主要是为了自己总结,并不是为了阅读量。