什么是迭代器?
在Rust中,迭代器是一种用于遍历集合(数组、向量、哈希表等)元素的机制。它提供了一种抽象的方式来处理序列中的元素,而无需直接访问底层的数据结构。Rust 提供了优雅的方式来处理集合类型,这也是我们之前文章里提到过的 Rust 的零成本抽象。下面我们在进行介绍的时候会同时让一些常用 API 和 JavaScript 的来进行对比学习。
迭代器的作用
迭代器的主要作用是提供一种统一的接口来遍历集合中的元素,并允许在遍历过程中进行各种操作。通过使用迭代器,我们可以以声明性的方式编写代码,而不是通过手动的循环和索引操作。
迭代器的使用有以下几个优点:
简洁性: 迭代器提供了一组统一的方法,使得代码更加简洁易读。
延迟计算: 迭代器是惰性计算的,它只在需要的时候计算下一个元素,避免了不必要的计算。
组合性: 不同的迭代器可以通过组合使用来实现复杂的操作,例如映射(map)、过滤(filter)、折叠(fold)等。
常用迭代器 API
创建迭代器
在Rust中,迭代器可以通过集合类型的方法来创建,也可以通过标准库提供的迭代器创建方法。以下是一些常用的迭代器创建方法:
从集合创建
use std::slice::Iter;
fn main() {
// 使用数组创建也行 let vec = [1, 2, 3, 4, 5];
let vec = vec![1, 2, 3, 4, 5];
// 创建一个迭代器来遍历向量
let iter_from_vec: Iter<'_, i32> = vec.iter();
// 然后可以调用迭代器对应的 API 方法了,比如 map、filter、fold 等方法
}
使用 iter_mut()
方法
use std::slice::IterMut;
fn main() {
let mut vec = vec![1, 2, 3, 4, 5];
// 创建一个可变迭代器,用于修改集合中的元素
let mut iter_mut: IterMut<'_, i32> = vec.iter_mut();
// 打印 Some(1)
println!("{:?}", iter_mut.next());
// 打印 Some(2)
println!("{:?}", iter_mut.next());
// 直至遍历结束返回 None
}
下面我们就来看看对应的 JavaScript 的示例代码
// 创建数组,数组本身就是一个迭代器
const numbers = [1, 2, 3, 4, 5];
// 使用数组的 values() 方法创建迭代器
const iterFromArray = numbers.values();
// 打印 {value: 1, done: false}
console.log(iterFromArray.next());
// 可以一直调用 next 方法直到遍历结束
// 直至遍历结束返回 {value: undefined, done: true}
迭代器方法
Rust的迭代器提供了丰富的方法来进行各种操作,以下是一些常用的迭代器方法和对应的代码示例:
Collect
它的作用是将迭代器的元素收集到一个集合类型中,可以是数组、向量、哈希表等,下面我们会举例子
map()
map()
方法用于对迭代器中的每个元素进行映射,和 JS 的 map 方法有点像
fn main() {
let vec = vec![1, 2, 3, 4, 5];
// 使用 collect 方法将迭代器转换回向量数组
let mapped_vec: Vec<i32> = vec.iter().map(|x| x * 2).collect();
// mapped_vec 现在是 [2, 4, 6, 8, 10]
println!("{:?}", mapped_vec)
}
对应的 JavaScript 代码
const numbers = [1, 2, 3, 4, 5];
const mappedArray = [...numbers].map(x => x * 2);
// mappedArray 现在是 [2, 4, 6, 8, 10]
filter()
filter()
方法用于对迭代器中的元素进行过滤,和 JS 的 filter 方法差不多。
fn main() {
let vec = vec![1, 2, 3, 4, 5];
// cloned 方法会复制一份新的数组,并且可以将 &T 类型转换为 T 类型
let filtered_vec: Vec<i32> = vec.iter().filter(|&x| x % 2 == 0).cloned().collect();
// vec 数组没变
println!("{:?}", vec);
// filtered_vec 现在是 [2, 4]
println!("{:?}", filtered_vec);
}
对应的 JavaScript 代码
const numbers = [1, 2, 3, 4, 5];
// js 也会创建一个新的数组
const filteredArray = [...numbers].filter(x => x % 2 === 0);
// filteredArray 现在是 [2, 4]
fold()
和 reduce()
可以对数组中的元素进行累加或累积等操作,对应 JS 中的 reduce 方法
fn main() {
let vec = vec![1, 2, 3, 4, 5];
let sum: i32 = vec.iter().cloned().fold(0, |acc, x| acc + x);
// 输出结果是 15
println!("{}", sum);
let sum: i32 = vec.iter().cloned().reduce(|acc, x| acc + x).unwrap();
// 输出结果是 15
println!("{}", sum);
}
对应的 JavaScript 代码
const numbers = [1, 2, 3, 4, 5];
// 使用 reduce()
const sum = [...numbers].reduce((acc, x) => acc + x, 0);
// sum 现在为 15
any()
和 all()
any()
方法用于检查迭代器中是否存在满足条件的元素,all()
方法用于检查是否所有元素都满足条件,分别对应的是 JS 的 some 和 every 方法。
fn main() {
let vec = vec![1, 2, 3, 4, 5];
// 看看是不是任意一个值是偶数
let has_even = vec.iter().any(|&x| x % 2 == 0);
// has_even 现在为 true
println!("{}", has_even);
// 看看是不是所有的值都是偶数
let all_even = vec.iter().all(|&x| x % 2 == 0);
// all_even 现在为 false
println!("{}", all_even);
}
const numbers = [1, 2, 3, 4, 5];
// 使用 some() 和 every()
const hasEven = [...numbers].some(x => x % 2 === 0);
// hasEven 现在为 true
const allEven = [...numbers].every(x => x % 2 === 0);
// allEven 现在为 false
zip()
zip()
方法用于将两个迭代器合并成一个。
fn main() {
let numbers = vec![1, 2, 3];
let words = vec!["one", "two", "three"];
let zipped: Vec<_> = numbers.iter().zip(words.iter()).collect();
// zipped 现在为 [(1, "one"), (2, "two"), (3, "three")]
println!("{:?}", zipped);
}
const numbers = [1, 2, 3];
// 使用 zip()(JS 没有直接的 zip 方法,这里用了 lodash 库来模拟)
const zipped = _.zip(numbers, words);
// zipped 现在为 [[1, "one"], [2, "two"], [3, "three"]]
// 或者用 map 来模拟也可以
const words = ["one", "two", "three"];
const zipped = numbers.map((value, index) => [value, words[index]]);
// zipped 现在为 [[1, "one"], [2, "two"], [3, "three"]]
enumerate()
枚举方法,一般用于在迭代的时候可以访问到迭代器的对应下标索引:
fn main() {
let numbers = vec![1, 2, 3];
for (i, number) in numbers.iter().enumerate() {
println!("{i}, {number}");
}
}
// 打印:
// Index: 0, Fruit: apple
// Index: 1, Fruit: banana
// Index: 2, Fruit: cherry
对应 JS 代码
const fruits = ["apple", "banana", "cherry"];
fruits.forEach((fruit, index) => {
console.log(`Index: ${index}, Fruit: ${fruit}`);
});
take
和 skip
take
方法用于获取迭代器的前 N 个元素,而 skip
方法用于跳过迭代器的前 N 个元素。
fn main() {
let numbers = vec![1, 2, 3, 4, 5];
// 调用 into_iter 方法会移动所有权,所以我们用 clone 方法克隆一份新数据
let first_three: Vec<_> = numbers.clone().into_iter().take(3).collect();
// first_three 现在为 [1, 2, 3]
println!("{:?}", first_three);
let last_two: Vec<_> = numbers.clone().into_iter().skip(3).collect();
// last_two 现在为 [4, 5]
println!("{:?}", last_two);
}
对应的 JS 代码
const numbers = [1, 2, 3, 4, 5];
const firstThree = numbers.slice(0, 3);
// firstThree 现在为 [1, 2, 3]
const lastTwo = numbers.slice(3);
// lastTwo 现在为 [4, 5]
chain()
chain
方法用于将两个迭代器连接成一个新的迭代器
fn main() {
let numbers = vec![1, 2, 3];
let more_numbers = vec![4, 5, 6];
let combined: Vec<_> = numbers.into_iter().chain(more_numbers).collect();
// combined 现在为 [1, 2, 3, 4, 5, 6]
println!("{:?}", combined);
}
对应的 JS 代码
const numbers = [1, 2, 3];
const moreNumbers = [4, 5, 6];
const combined = numbers.concat(moreNumbers);
// combined 现在为 [1, 2, 3, 4, 5, 6]
for_each()
遍历每一个元素并做一些操作,不返回值,对应于 JS 的 forEach 方法,只不过在迭代中没法拿到下标索引
fn main() {
let numbers = [1, 2, 3, 4, 5];
numbers.iter().for_each(|val| {
println!("val: {val}");
});
}
cloned
和 copied
cloned
方法用于创建一个新的迭代器,其中每个元素都是原始迭代器中元素的克隆,而 copied
方法则用于创建一个新的迭代器,其中每个元素都是原始迭代器中元素的复制。
cloned
适用于任何实现了 Clone
trait 的类型,它调用 clone
方法创建元素的克隆,
copied
适用于实现了 Copy
trait 的类型,它通过位复制的方式创建元素的副本,避免了显式的 clone
调用
#[derive(Clone, Copy, Debug)]
struct Number {
value: i32,
}
fn main() {
let numbers = vec![
Number { value: 1 },
Number { value: 2 },
Number { value: 3 },
];
let mut cloned: Vec<Number> = numbers.iter().cloned().collect();
cloned[0].value = 10;
//numbers: [Number { value: 1 }, Number { value: 2 }, Number { value: 3 }], cloned: [Number { value: 10 }, Number { value: 2 }, Number { value: 3 }]
println!("numbers: {:?}, cloned: {:?}", numbers, cloned);
let mut copied: Vec<_> = numbers.iter().copied().collect();
copied[1].value = 10;
// numbers: [Number { value: 1 }, Number { value: 2 }, Number { value: 3 }], copied: [Number { value: 1 }, Number { value: 10 }, Number { value: 3 }]
println!("numbers: {:?}, copied: {:?}", numbers, copied);
}
其它 API
另外还有 max, 对应 JS 的 max 方法,min 对应 JS 的 min 方法, rev 对应 JS 的 reverse 方法,count 方法对应于 JS 数组的 .length ,另外还有很多很多的方法,这里列举不完了,大家可以在实战中自己去尝试
for_each 对应
迭代器的实战例子
为了更好地理解迭代器在实际应用中的威力,让我们搞一个更复杂的例子:从一个日志文件中提取关键信息并进行统计。
假设我们有一个日志文件,其中包含了用户的访问记录。每行日志的格式如下:
timestamp,username,action
比如根目录下有一个 log.txt 文件,里面的内容是:
2024-03-10,PuffMeow,exp
我们的目标是统计每个用户的访问次数。首先,我们将文件内容读取并解析为结构体:
use std::fs::File;
use std::io::{self, BufRead};
use std::path::Path;
#[derive(Debug)]
struct LogEntry {
timestamp: String,
username: String,
action: String,
}
// 用于新建结构体
impl LogEntry {
fn new(timestamp: &str, username: &str, action: &str) -> Self {
LogEntry {
timestamp: timestamp.to_string(),
username: username.to_string(),
action: action.to_string(),
}
}
}
接下来,我们使用迭代器从文件中读取日志并进行处理:
fn main() -> io::Result<()> {
let file_path = "log.txt";
let file = File::open(file_path)?;
// 使用 BufReader 封装文件读取器
let reader = BufReader::new(file);
// 使用 lines() 方法获取迭代器,每次迭代返回一行日志
let log_entries: Vec<LogEntry> = reader
.lines()
.filter_map(|line| {
// 将每行日志解析为 LogEntry 结构体
let line = line.ok()?;
let parts: Vec<&str> = line.split(',').collect();
if parts.len() == 3 {
Some(LogEntry::new(parts[0], parts[1], parts[2]))
} else {
None
}
})
.collect();
// 统计每个用户的访问次数
let user_access_count: HashMap<&str, usize> = log_entries
.iter()
.map(|entry| entry.username.as_str())
.fold(HashMap::new(), |mut acc, username| {
*acc.entry(username).or_insert(0) += 1;
acc
});
// 输出统计结果
// 输出结果:
// User: PuffMeow, Access Count: 1
for (username, count) in user_access_count {
println!("User: {}, Access Count: {}", username, count);
}
Ok(())
}
总结
Rust 的迭代器是一种强大的工具,提供了一套很方便的 API,大部分功能基本不用我们自己实现了,使得处理集合数据变得更加简单、安全和高效。通过使用迭代器,我们能够以声明性的方式编写代码,将注意力集中在业务逻辑上,而不是底层的循环和索引操作上。好了,今天的文章先到这儿吧,码字一个多小时做的学习总结,抽象。