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

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

iter() 不转移所有权

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

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

例如:

复制代码
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() 不会转移集合的所有权。相反,它创建一个迭代器,该迭代器借用集合的内容:

复制代码
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)的迭代器。其允许在迭代过程中修改集合中的元素。

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

如下:

复制代码
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)集合,并拥有其元素的所有权。这通常用于在迭代时转移集合中元素的所有权。

如下:

复制代码
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,其中包含一些数字的引用,现在想要迭代这些数字的拷贝而不是引用本身:

复制代码
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);
    }
}

输出为:

复制代码
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用于对迭代器中的 每个元素 应用某个函数/执行某项(会发生修改的)操作,并返回一个新的迭代器。

例如:

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

let doubles = numbers.iter().map(|x| x * 2);
// doubles将包含[2, 4, 6, 8, 10]

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

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

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

输出:

复制代码
2
4
6
8
10

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

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

fold需要两个参数:

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

例如:

复制代码
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,最终将数组所有元素求和。

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

复制代码
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用于过滤迭代器中的元素,只保留满足条件的元素。

例如:

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

let evens = numbers.iter().filter(|&x| x % 2 == 0);
// evens将只包含[2, 4]

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);
    }
}

输出:

复制代码
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() 来尝试解析每个字符串,并仅保留那些成功解析为整数的元素:

复制代码
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()消费器可以实现集合类型转化

复制代码
fn main() {
    println!("{:?}", vec![1, 2, 3].iter().collect::<Vec<_>>()); // [1, 2, 3]
}
  • sum():计算迭代器元素的和。

    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():找到迭代器最小/最大元素。

    fn main() {
    let max = [1, 2, 3].iter().max();
    println!("{:?}", max.unwrap()); // 3
    }

  • count():统计迭代器元素数量。

    fn main() {
    let count = [1, 2, 3, 0, 6].iter().count();
    println!("{:?}", count); // 5
    }

  • any()/all():是否存在(某个元素)/所有元素满足条件。

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

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

fn main() {
    let numbers = [2, 4, 6];

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

    println!("{:?}", result); // true, 即判断Vec中所有元素是否都能被2整除
}
  • find():找到第一个满足条件的元素并输出,否则返回None

    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

本文由mdnice多平台发布

相关推荐
BingoGo1 分钟前
使用 PHP 和 WebSocket 构建实时聊天应用:完整指南
后端·php
JaguarJack16 分钟前
使用 PHP 和 WebSocket 构建实时聊天应用 完整指南
后端·php
CodeSheep1 小时前
中国四大软件外包公司
前端·后端·程序员
千寻技术帮1 小时前
10370_基于Springboot的校园志愿者管理系统
java·spring boot·后端·毕业设计
风象南1 小时前
Spring Boot 中统一同步与异步执行模型
后端
聆风吟º1 小时前
【Spring Boot 报错已解决】彻底解决 “Main method not found in class com.xxx.Application” 报错
java·spring boot·后端
乐茵lin1 小时前
golang中 Context的四大用法
开发语言·后端·学习·golang·编程·大学生·context
步步为营DotNet1 小时前
深度探索ASP.NET Core中间件的错误处理机制:保障应用程序稳健运行
后端·中间件·asp.net
bybitq2 小时前
Go中的闭包函数Closure
开发语言·后端·golang
吴佳浩9 小时前
Python入门指南(六) - 搭建你的第一个YOLO检测API
人工智能·后端·python