MK
摩柯社区 - 一个极简的技术知识社区
AI 面试

Rust for 循环遍历不同集合的方式

2022-04-116.4k 阅读

Rust 中 for 循环基础

在 Rust 编程语言里,for 循环是一种非常常用的结构,用于迭代集合中的元素。for 循环的基本语法如下:

for item in collection {
    // 在这里对 item 进行操作
}

这里的 collection 可以是各种实现了 IntoIterator trait 的类型,而 item 则是每次迭代从 collection 中取出的元素。

遍历数组(Array)

数组是 Rust 中一种固定大小的数据集合,其元素类型相同。例如,我们有一个包含整数的数组:

let numbers = [1, 2, 3, 4, 5];
for number in numbers.iter() {
    println!("Number: {}", number);
}

在这个例子中,我们使用 iter() 方法来创建一个迭代器。数组本身实现了 IntoIterator trait,iter() 方法会返回一个 Iterator,它按顺序生成数组中的每一个元素。注意,这里 number 的类型是 &i32,因为 iter() 返回的是一个借用的迭代器,这样可以避免在遍历过程中移动数组中的元素。

如果我们希望在遍历过程中修改数组元素,我们可以使用 iter_mut() 方法:

let mut numbers = [1, 2, 3, 4, 5];
for number in numbers.iter_mut() {
    *number += 1;
    println!("Number: {}", number);
}

这里 number 的类型是 &mut i32,通过解引用 *number,我们可以修改数组中的元素。

遍历向量(Vector)

向量(Vec)是 Rust 中动态大小的数组,其大小可以在运行时改变。遍历向量的方式与数组类似:

let mut vec_numbers = vec![1, 2, 3, 4, 5];
for number in &vec_numbers {
    println!("Number: {}", number);
}

这里我们使用 &vec_numbers,这会自动调用 iter() 方法,因为 Vec 实现了 IntoIterator trait。如果我们想要修改向量中的元素,同样可以使用 iter_mut()

let mut vec_numbers = vec![1, 2, 3, 4, 5];
for number in vec_numbers.iter_mut() {
    *number += 1;
    println!("Number: {}", number);
}

遍历切片(Slice)

切片(&[T])是对一块连续内存区域的引用,它可以指向数组或向量的一部分。遍历切片也很简单:

let numbers = [1, 2, 3, 4, 5];
let slice = &numbers[1..3];
for number in slice.iter() {
    println!("Number: {}", number);
}

这里我们创建了一个指向 numbers 数组部分元素的切片 slice,通过 iter() 方法遍历切片中的元素。同样,如果需要修改切片中的元素,可以使用 iter_mut()

let mut numbers = [1, 2, 3, 4, 5];
let slice = &mut numbers[1..3];
for number in slice.iter_mut() {
    *number += 1;
    println!("Number: {}", number);
}

遍历字符串(String)

Rust 中的字符串类型有两种:String&strString 是可增长、可变的字符串类型,而 &str 是字符串切片,是不可变的。

遍历 String

let s = String::from("hello");
for c in s.chars() {
    println!("Character: {}", c);
}

这里我们使用 chars() 方法来遍历 String 中的每一个字符。chars() 方法会把字符串拆分成一个个独立的 char 类型。

如果我们想按字节遍历 String,可以使用 bytes() 方法:

let s = String::from("hello");
for byte in s.bytes() {
    println!("Byte: {}", byte);
}

遍历 &str

&str 类型同样支持 chars()bytes() 方法来遍历字符和字节:

let s = "hello";
for c in s.chars() {
    println!("Character: {}", c);
}

遍历哈希表(HashMap)

哈希表(HashMap)是 Rust 标准库中提供的一种键值对集合。要遍历 HashMap,我们可以使用 for 循环结合 iter() 方法:

use std::collections::HashMap;

let mut scores = HashMap::new();
scores.insert(String::from("Alice"), 100);
scores.insert(String::from("Bob"), 200);

for (key, value) in scores.iter() {
    println!("{}: {}", key, value);
}

在这个例子中,scores.iter() 返回一个迭代器,每次迭代产生一个 (&K, &V) 类型的元组,这里 K 是键的类型,V 是值的类型。

如果我们想要在遍历过程中修改 HashMap,可以使用 iter_mut()

use std::collections::HashMap;

let mut scores = HashMap::new();
scores.insert(String::from("Alice"), 100);
scores.insert(String::from("Bob"), 200);

for (key, value) in scores.iter_mut() {
    if key == "Alice" {
        *value += 10;
    }
}

遍历 B 树映射(BTreeMap)

B 树映射(BTreeMap)也是一种键值对集合,与 HashMap 不同的是,BTreeMap 会按键的顺序存储键值对。遍历 BTreeMap 的方式与 HashMap 类似:

use std::collections::BTreeMap;

let mut scores = BTreeMap::new();
scores.insert(1, String::from("Alice"));
scores.insert(2, String::from("Bob"));

for (key, value) in scores.iter() {
    println!("{}: {}", key, value);
}

同样,scores.iter() 返回一个迭代器,每次迭代产生一个 (&K, &V) 类型的元组。如果需要修改 BTreeMap,可以使用 iter_mut()

遍历链表(LinkedList)

链表(LinkedList)是一种动态数据结构,由节点组成,每个节点包含数据和指向下一个节点的指针。在 Rust 中,std::collections::LinkedList 提供了链表的实现。

use std::collections::LinkedList;

let mut list = LinkedList::new();
list.push_back(1);
list.push_back(2);
list.push_back(3);

for number in list.iter() {
    println!("Number: {}", number);
}

这里我们使用 iter() 方法遍历链表中的元素。如果需要修改链表中的元素,可以使用 iter_mut()

use std::collections::LinkedList;

let mut list = LinkedList::new();
list.push_back(1);
list.push_back(2);
list.push_back(3);

for number in list.iter_mut() {
    *number += 1;
    println!("Number: {}", number);
}

遍历迭代器适配器(Iterator Adapters)

Rust 的迭代器有很多强大的适配器方法,这些方法可以对迭代器进行转换、过滤等操作。例如,我们可以使用 filter() 方法过滤掉不符合条件的元素,然后再遍历:

let numbers = vec![1, 2, 3, 4, 5];
for number in numbers.iter().filter(|&&n| n % 2 == 0) {
    println!("Even number: {}", number);
}

在这个例子中,filter(|&&n| n % 2 == 0) 会过滤掉所有奇数,只留下偶数,然后我们遍历剩下的偶数。

另一个常用的适配器方法是 map(),它可以对迭代器中的每个元素应用一个函数:

let numbers = vec![1, 2, 3, 4, 5];
for squared in numbers.iter().map(|&n| n * n) {
    println!("Squared: {}", squared);
}

这里 map(|&n| n * n) 会将每个元素平方,然后我们遍历平方后的结果。

遍历嵌套集合

在实际编程中,我们经常会遇到嵌套集合的情况,比如一个向量中包含多个向量,或者一个哈希表中值的类型是另一个哈希表。以嵌套向量为例:

let nested_vec = vec![vec![1, 2], vec![3, 4], vec![5, 6]];
for inner_vec in nested_vec.iter() {
    for number in inner_vec.iter() {
        println!("Number: {}", number);
    }
}

这里外层循环遍历 nested_vec 中的每个内部向量,内层循环遍历每个内部向量中的元素。

对于嵌套哈希表,假设我们有一个哈希表,其值又是一个哈希表:

use std::collections::HashMap;

let mut outer_map = HashMap::new();
let mut inner_map1 = HashMap::new();
inner_map1.insert(String::from("a"), 1);
let mut inner_map2 = HashMap::new();
inner_map2.insert(String::from("b"), 2);

outer_map.insert(String::from("key1"), inner_map1);
outer_map.insert(String::from("key2"), inner_map2);

for (outer_key, inner_map) in outer_map.iter() {
    println!("Outer key: {}", outer_key);
    for (inner_key, value) in inner_map.iter() {
        println!("Inner key: {}, Value: {}", inner_key, value);
    }
}

遍历自定义集合

我们还可以创建自己的集合类型,并实现 IntoIterator trait 来使其可以被 for 循环遍历。假设我们有一个简单的自定义结构体 MyList

struct MyList<T> {
    data: Vec<T>,
}

impl<T> MyList<T> {
    fn new() -> Self {
        MyList { data: Vec::new() }
    }

    fn push(&mut self, item: T) {
        self.data.push(item);
    }
}

impl<T> IntoIterator for MyList<T> {
    type Item = T;
    type IntoIter = std::vec::IntoIter<T>;

    fn into_iter(self) -> Self::IntoIter {
        self.data.into_iter()
    }
}

let mut my_list = MyList::new();
my_list.push(1);
my_list.push(2);
my_list.push(3);

for number in my_list {
    println!("Number: {}", number);
}

在这个例子中,我们为 MyList 结构体实现了 IntoIterator trait。IntoIterator 有两个关联类型:Item 表示迭代器产生的元素类型,IntoIter 表示迭代器的类型。通过实现 into_iter 方法,我们将 MyList 中的 Vec 的迭代器返回,这样 MyList 就可以被 for 循环遍历了。

循环控制

for 循环中,我们可以使用 breakcontinue 语句来控制循环的执行。break 语句会立即终止循环:

let numbers = vec![1, 2, 3, 4, 5];
for number in numbers.iter() {
    if *number == 3 {
        break;
    }
    println!("Number: {}", number);
}

在这个例子中,当 number 等于 3 时,break 语句会终止循环,所以只会输出 1 和 2。

continue 语句会跳过当前迭代,继续下一次迭代:

let numbers = vec![1, 2, 3, 4, 5];
for number in numbers.iter() {
    if *number == 3 {
        continue;
    }
    println!("Number: {}", number);
}

这里当 number 等于 3 时,continue 语句会跳过当前迭代,继续下一次迭代,所以会输出 1、2、4 和 5。

遍历与所有权

在 Rust 中,所有权是一个非常重要的概念。当我们遍历集合时,需要注意所有权的转移和借用规则。例如,当我们使用 for 循环遍历一个向量并消耗其中的元素时,所有权会发生转移:

let numbers = vec![1, 2, 3, 4, 5];
for number in numbers {
    println!("Number: {}", number);
}
// 这里 numbers 已经被消耗,不能再使用

在这个例子中,numbers 的所有权转移到了 for 循环中的 number,循环结束后,numbers 不再有效。

而当我们使用 iter() 方法时,我们只是借用了集合中的元素,集合的所有权仍然归原变量所有:

let numbers = vec![1, 2, 3, 4, 5];
for number in numbers.iter() {
    println!("Number: {}", number);
}
// 这里 numbers 仍然可以使用

并行遍历

在 Rust 中,我们可以使用 rayon 库来实现并行遍历集合。rayon 提供了 ParallelIterator trait,许多集合类型都可以通过扩展方法转换为并行迭代器。

首先,在 Cargo.toml 文件中添加 rayon 依赖:

[dependencies]
rayon = "1.5.1"

然后,我们可以这样使用并行遍历:

use rayon::prelude::*;

let numbers = (1..10000).collect::<Vec<_>>();
let sum: i32 = numbers.par_iter().sum();
println!("Sum: {}", sum);

在这个例子中,par_iter() 方法将 Vec 转换为并行迭代器,sum() 方法会并行计算所有元素的和。

遍历性能考量

不同的集合类型在遍历性能上可能会有差异。例如,数组和向量的遍历通常比较快,因为它们在内存中是连续存储的,CPU 可以利用缓存预取技术提高访问效率。而链表由于节点在内存中是分散存储的,遍历链表可能会有更多的缓存不命中,导致性能相对较低。

另外,使用迭代器适配器方法时,也要注意性能。例如,过多的链式适配器调用可能会增加代码的复杂性和运行时开销。在一些性能敏感的场景下,可能需要手动展开循环来优化性能。

同时,并行遍历虽然可以利用多核 CPU 的优势提高计算速度,但也会引入额外的线程创建、同步等开销。在数据量较小的情况下,并行遍历可能反而会降低性能,所以需要根据实际情况进行权衡。

在 Rust 中,for 循环提供了一种简洁而强大的方式来遍历各种集合。通过深入理解不同集合的遍历方式,以及相关的所有权、性能等概念,开发者可以编写出高效、安全的代码。无论是简单的数组遍历,还是复杂的嵌套集合、并行遍历,Rust 都提供了丰富的工具和方法来满足各种需求。