Rust开发⼲货集(1)--迭代器与消费器

2024-01-09 15:24:16 浏览数 (1)

本内容是对 Rust开发干货集[1] 的实践与扩展.

iter() 不转移所有权

先简单解释下什么叫"转移所有权":

在 Rust 中,"转移所有权"(Ownership Transfer)是一种核心概念,它涉及变量和数据的所有权从一个实体转移到另一个实体。这种机制帮助 Rust 在编译时期管理内存安全,避免悬挂指针和内存泄漏等问题。

例如:

代码语言:javascript复制
fn main() {
    let s1 = String::from("hello");
    let s2 = s1; // 所有权从 s1 转移到 s2

    // println!("{}", s1); // 这行会引发编译错误,因为 s1 不再拥有数据的所有权. 报错: error[E0382]: borrow of moved value: `s1`
    println!("{}", s2); // 正确,s2 现在拥有数据的所有权
}

当 s1 被赋值给 s2 时,s1 的所有权被转移给了 s2。这意味着 s1 不再有效,因此接下来如果使用 s1 将导致编译错误。

iter() 在 Rust 中用于创建集合的迭代器,比如在数组或向量上。iter() 不会转移集合的所有权。相反,它创建一个迭代器,该迭代器借用集合的内容:

代码语言:javascript复制
fn main() {
    let v = vec![1, 2, 3];

    for i in v.iter() {
        println!("{}", i);
    }

    // v 仍然有效,因为 iter() 没有取得所有权
    println!("Vector: {:?}", v);
}

上例中,v.iter() 创建了一个迭代器,但 v 的所有权没有改变。因此,在迭代之后,仍然可以使用 v

这说明iter() 不转移所有权(因为所有权转移意味着原始变量不再有效)

另外几种创建迭代器的方法: iter_mut()into_iter()

iter_mut()

iter_mut() 方法用于创建一个可变借用(mutable borrow)的迭代器。其允许在迭代过程中修改集合中的元素。

(所有权并没有发生转移)

如下:

代码语言:javascript复制
fn main() {
    let mut v = vec![1, 2, 3];

    for i in v.iter_mut() {
        *i *= 2; // 将每个元素乘以 2
        println!("{}", i);
    }

    println!("{:?}", v); // 输出: [2, 4, 6]
}

v.iter_mut() 创建了一个可变迭代器,允许修改向量 v 中的每个元素

into_iter()

into_iter() 方法用于创建一个取得所有权(ownership)的迭代器---这意味着迭代器会消耗(consume)集合,并拥有其元素的所有权。这通常用于在迭代时转移集合中元素的所有权。

如下:

代码语言:javascript复制
fn main() {
    let v = vec![1, 2, 3];

    for i in v.into_iter() {
        println!("{}", i); // 打印每个元素
    }

    // println!("{:?}", v); // 这行会编译错误,因为 v 的所有权已经被移动; error[E0382]: borrow of moved value: `v`
}

v.into_iter() 创建了一个获取 v 所有权的迭代器。迭代后,v 不再有效,因为它的所有权已经被迭代器 into_iter() 消耗。

into_iter() 会转移所有权。它创建一个获取集合所有权的迭代器,允许在迭代时转移集合中元素的所有权。一旦使用了 into_iter(),原始集合将不再有效,因为它的所有权已经被迭代器取得。

iter_mut() 用于需要修改集合中元素的场景,但并不转移所有权; 而 into_iter() 用于需要转移元素所有权的场景。

iter()cloned()方法

iter() 方法用于创建一个不可变引用的迭代器,而 cloned() 是这类迭代器的一个方法。cloned() 的作用是将迭代器中的每个元素通过调用其 clone 方法来创建一个新的实例。这通常用于当拥有一个包含引用的迭代器,但需要迭代器中的值的拷贝时。

cloned() 的作用:

  • 创建元素的拷贝cloned() 方法适用于迭代器的元素实现了 Clone trait。它会为每个元素创建一个新的实例,这是通过调用每个元素的 clone 方法实现的。
  • 不转移所有权:由于 cloned() 仅仅是创建元素的副本,它不会改变原始数据的所有权。

如下例: 假设有一个Vec,其中包含一些数字的引用,现在想要迭代这些数字的拷贝而不是引用本身:

代码语言:javascript复制
use std::any::type_name;

fn print_type_of<T>(_: &T) {
    println!("变量类型为:{}", type_name::<T>());
}

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

    for num in nums.iter() {
        println!("{}", num); // 输出: 1, 2, 3
        print_type_of(&num);
    }

    println!("-----分界线------");
    let nums_cloned: Vec<i32> = nums.iter().cloned().collect();

    println!("{:?}", nums_cloned); // 输出: [1, 2, 3]

    for num in nums_cloned {
        println!("{}", num); // 输出: 1, 2, 3
        print_type_of(&num);
    }
}

输出为:

代码语言:javascript复制
1
变量类型为:&i32
2
变量类型为:&i32
3
变量类型为:&i32
-----分界线------
[1, 2, 3]
1
变量类型为:i32
2
变量类型为:i32
3
变量类型为:i32

在上例中,使用 cloned() 方法,可以将这些引用转换为实际的数字拷贝,从而创建一个新的 Vec<i32>。这样,nums_cloned 就包含了 nums 中每个元素的拷贝,而不是引用。

cloned() 在 Rust 中用于从迭代器中创建元素的拷贝,特别是当有一个包含引用的迭代器 并希望获得实际值的拷贝时。它是处理引用集合时常用的便捷方法。

iter_mut() 有没有 cloned()方法?

iter_mut() 方法返回的迭代器是一个可变引用的迭代器。由于 cloned() 方法是用于拷贝迭代器中的值,它通常与不可变引用的迭代器(如由 iter() 返回的迭代器)一起使用。cloned() 方法适用于那些实现了 Clone trait 的元素,它会创建每个元素的拷贝。

对于 iter_mut() 返回的迭代器,由于它提供的是对元素的可变引用(&mut T),使用 cloned() 方法是不适当的,也不符合 Rust 的安全性原则。可变引用的目的是允许修改集合中的元素,而不是创建它们的拷贝。如果需要修改元素并且需要它们的拷贝,应该首先通过其他方式创建拷贝,然后对这些拷贝进行修改。

因此,在实际的 Rust 编程实践中,iter_mut() 迭代器上不会使用 cloned() 方法。如果需要元素的拷贝,应该使用 iter() 方法来创建一个不可变引用的迭代器,然后在该迭代器上使用 cloned()

map/fold(reduce)/filter的作用

更多可参考 初探函数式编程---以Map/Reduce/Filter为例[2]

map用于对迭代器中的 每个元素 应用某个函数/执行某项(会发生修改的)操作,并返回一个新的迭代器。

例如:

代码语言:javascript复制
let numbers = [1, 2, 3, 4, 5];

let doubles = numbers.iter().map(|x| x * 2);
// doubles将包含[2, 4, 6, 8, 10]
代码语言:javascript复制
fn main() {
    let numbers = [1, 2, 3, 4, 5];

    let doubles = numbers.iter().map(|x| x * 2);

    for x in doubles {
        println!("{}", x);
    }
}

输出:

代码语言:javascript复制
2
4
6
8
10

fold用于将迭代器中的元素进行累积计算,其基本语法是:

代码语言:javascript复制
iter.fold(init, |acc, x| {
   // acc是累积值,x是当前元素
   // 返回更新后的acc
})

fold需要两个参数:

  • init:初始累积值
  • 闭包:接收当前累积值acc和元素x,返回更新后的acc

例如:

代码语言:javascript复制
fn main() {
    let numbers = [1, 2, 3, 4, 5];

    let sum = numbers.iter().fold(0, |acc, x| acc   x);

    println!("Sum is: {}", sum); // Sum is: 15
}

这里fold的init值为0,闭包中每次将acc和x相加,返回更新后的acc,最终将数组所有元素求和。

另一个例子,连接字符串:

代码语言:javascript复制
fn main() {
    let strings = ["a", "b", "c"];

    let concat = strings.iter().fold(String::new(), |acc, s| acc   s);

    println!("Concat is: {}", concat); // Concat is: abc
}

fold可以将迭代中的元素进行任意累积计算,常见用法包括求和、乘积、字符串连接等。fold()消费器可以实现reduce逻辑

filter用于过滤迭代器中的元素,只保留满足条件的元素。

例如:

代码语言:javascript复制
let numbers = [1, 2, 3, 4, 5];

let evens = numbers.iter().filter(|&x| x % 2 == 0);
// evens将只包含[2, 4]
代码语言:javascript复制
fn main() {
    let numbers = [1, 2, 3, 4, 5];

    let evens: Vec<_> = numbers.iter().filter(|&x| x % 2 == 0).collect();

    for x in evens {
        println!("{}", x);
    }
}

输出:

代码语言:javascript复制
2
4

filter_map() 可以同时完成转换和过滤

filter_map() 方法结合了过滤(filter)和映射(map)功能。

这个方法接收一个闭包,该闭包作用于迭代器的每个元素,并返回 Option<T> 类型。filter_map() 然后自动过滤掉所有返回 None 的元素,并将所有 Some 包裹的值提取出来,形成一个新的迭代器。

  • 过滤和转换filter_map() 允许同时对迭代器的元素进行过滤和转换。如果闭包返回 Some(value),则 value 被包含在新迭代器中;如果闭包返回 None,则该元素被过滤掉。
  • 可选的转换:与 map() 相比,filter_map() 允许根据元素的值选择性地包含或排除元素,而不是简单地映射每个元素到另一个值。

举个例子, 假设有一个字符串类型的向量,想将其中的每个字符串转换为整数。但不是所有的字符串都可以转换为整数(例如,某些字符串可能包含非数字字符,如"1ab")。

在这种情况下,就可以使用 filter_map() 来尝试解析每个字符串,并仅保留那些成功解析为整数的元素:

代码语言:javascript复制
fn main() {
    let strings = vec!["3", "seven", "8", "10"];

    let numbers: Vec<i32> = strings
        .into_iter()
        .filter_map(|s| s.parse::<i32>().ok())
        .collect();

    println!("{:?}", numbers); // 输出: [3, 8, 10]
}

在上例中,filter_map() 尝试将每个字符串转换为 i32。如果 parse 方法成功(即返回 Ok(value)),filter_map()Some(value) 包装的 value 加入到新的迭代器中。如果 parse 失败(即返回 Err),filter_map() 则通过 ok() 方法将 Err 转换为 None,从而过滤掉这些元素。

通过这种方式,filter_map() 使得同时进行过滤和映射变得简单而高效。

另外一些消费器

上面介绍的 mapfoldfilter ,都属于消费器, 消费器在Rust中是指能够消费迭代器的类型

另外还有一些常用的消费器,包括:

  • collect():将迭代器收集到集合类型如Vec/String中

collect()消费器可以实现集合类型转化

代码语言:javascript复制
fn main() {
    println!("{:?}", vec![1, 2, 3].iter().collect::<Vec<_>>()); // [1, 2, 3]
}
  • sum():计算迭代器元素的和。
代码语言:javascript复制
fn main() {
    // sum()的返回类型依赖于迭代器的元素类型,这里元素是i32,所以需要明确指定sum_value的类型为i32
    let sum_value1: i32 = [1, 2, 3].iter().sum();
    println!("{:?}", sum_value1); // 6

    // 或者使用泛型参数指定
    let sum_value2 = [1, 2, 3].iter().sum::<i32>();
    println!("{:?}", sum_value2); // 6
}
  • min()/max():找到迭代器最小/最大元素。
代码语言:javascript复制
fn main() {
    let max = [1, 2, 3].iter().max();
    println!("{:?}", max.unwrap()); // 3
}
  • count():统计迭代器元素数量。
代码语言:javascript复制
fn main() {
    let count = [1, 2, 3, 0, 6].iter().count();
    println!("{:?}", count); // 5
}
  • any()/all():是否存在(某个元素)/所有元素满足条件。

any()消费器可以查找是否存在满⾜条件的元素,迭代器是惰性的,any消费器可能不需要遍历Iterator

代码语言:javascript复制
fn main() {
    let exists = [1, 2, 3].iter().any(|x| *x == 2);
    println!("{:?}", exists); // true, 即判断Vec中是否有值为2的元素
}
代码语言:javascript复制
fn main() {
    let numbers = [2, 4, 6];

    let result = numbers.iter().all(|x| x % 2 == 0);

    println!("{:?}", result); // true, 即判断Vec中所有元素是否都能被2整除
}
  • find():找到第一个满足条件的元素并输出,否则返回None
代码语言:javascript复制
fn main() {
    let numbers = [1, 2, 3, 4];

    // 查找numbers中第一个等于3的元素,找到后返回Some(3)
    let result = numbers.iter().find(|&x| *x == 3);
    println!("{:?}", result); // Some(3)

    // 如果没有找到满足条件的元素,则会返回None:
    let result = numbers.iter().find(|&x| *x == 5);
    println!("{:?}", result); // None
}

这些消费器都会完全消费迭代器,将其结果返回。它们常用于迭代器计算的最后阶段,将迭代结果转化为具体值。

参考资料

[1]

Rust开发干货集: https://github.com/rustaceanclub/rust-slides/tree/master/20191216-GB-Rust开发干货集

[2]

初探函数式编程---以Map/Reduce/Filter为例: https://juejin.cn/post/7270823313809752076

0 人点赞