使用迭代器处理元素序列
ch13-02-iterators.md
commit eabaaaa90ee6937db3690dc56f739116be55ecb2
迭代器模式允许你依次对一个序列中的项执行某些操作。迭代器(iterator)负责遍历序列中的每一项并确定序列何时结束的逻辑。使用迭代器时,你无需自己重新实现这些逻辑。
在 Rust 中,迭代器是 惰性的(lazy),这意味着在调用消费迭代器的方法之前不会执行任何操作。例如,示例 13-10 中的代码通过调用定义于 Vec<T>
上的 iter
方法在一个 vector v1
上创建了一个迭代器。这段代码本身并没有执行任何有用的操作。
fn main() { let v1 = vec![1, 2, 3]; let v1_iter = v1.iter(); }
迭代器被储存在 v1_iter
变量中。一旦创建迭代器之后,可以选择用多种方式利用它。在第三章的示例 3-5 中,我们使用 for
循环来遍历一个数组并在每一个项上执行了一些代码。在底层它隐式地创建并接着消费了一个迭代器,不过直到现在我们都一笔带过了它具体是如何工作的。
示例 13-11 中的例子将迭代器的创建和 for
循环中的使用分开。当 for
循环使用 v1_iter
中的迭代器时,迭代器中的每一个元素都会用于循环的一次迭代,并打印出每个值。
fn main() { let v1 = vec![1, 2, 3]; let v1_iter = v1.iter(); for val in v1_iter { println!("Got: {val}"); } }
在标准库中没有提供迭代器的语言中,我们可能会使用一个从 0 开始的索引变量,使用这个变量索引 vector 中的值,并循环增加其值直到达到 vector 中的元素总量。
迭代器为我们处理了所有这些逻辑,这减少了重复代码并消除了潜在的混乱。另外,迭代器的实现方式提供了对多种不同的序列使用相同逻辑的灵活性,而不仅仅是像 vector 这样可索引的数据结构。让我们看看迭代器是如何做到这些的。
Iterator
trait 和 next
方法
迭代器都实现了一个叫做 Iterator
的定义于标准库的 trait。这个 trait 的定义看起来像这样:
#![allow(unused)] fn main() { pub trait Iterator { type Item; fn next(&mut self) -> Option<Self::Item>; // 此处省略了方法的默认实现 } }
注意这里有一个我们还未讲到的新语法:type Item
和 Self::Item
,它们定义了 trait 的 关联类型(associated type)。第二十章会深入讲解关联类型,不过现在只需知道这段代码表明实现 Iterator
trait 要求同时定义一个 Item
类型,这个 Item
类型被用作 next
方法的返回值类型。换句话说,Item
类型将是迭代器返回元素的类型。
next
是 Iterator
实现者被要求定义的唯一方法:next
方法,该方法每次返回迭代器中的一个项,封装在 Some
中,并且当迭代完成时,返回 None
。
可以直接调用迭代器的 next
方法;示例 13-12 展示了对由 vector 创建的迭代器重复调用 next
方法时返回的值。
文件名:src/lib.rs
#[cfg(test)]
mod tests {
#[test]
fn iterator_demonstration() {
let v1 = vec![1, 2, 3];
let mut v1_iter = v1.iter();
assert_eq!(v1_iter.next(), Some(&1));
assert_eq!(v1_iter.next(), Some(&2));
assert_eq!(v1_iter.next(), Some(&3));
assert_eq!(v1_iter.next(), None);
}
}
注意我们需要将 v1_iter
声明为可变的:在迭代器上调用 next
方法会改变迭代器内部的状态,该状态用于跟踪迭代器在序列中的位置。换句话说,代码 消费(consume)了,或者说用尽了迭代器。每一次 next
调用都会从迭代器中消费一个项。使用 for
循环时无需使 v1_iter
可变因为 for
循环会获取 v1_iter
的所有权并在后台使 v1_iter
可变。
还需要注意的是,从 next
调用中获取的值是对 vector 中值的不可变引用。iter
方法生成一个不可变引用的迭代器。如果我们需要一个获取 v1
所有权并返回拥有所有权的迭代器,则可以调用 into_iter
而不是 iter
。类似地,如果我们希望迭代可变引用,可以调用 iter_mut
而不是 iter
。
消费迭代器的方法
Iterator
trait 有一系列不同的由标准库提供默认实现的方法;你可以在 Iterator
trait 的标准库 API 文档中找到所有这些方法。一些方法在其定义中调用了 next
方法,这也就是为什么在实现 Iterator
trait 时要求实现 next
方法的原因。
这些调用 next
方法的方法被称为 消费适配器(consuming adaptors),因为调用它们会消耗迭代器。一个消费适配器的例子是 sum
方法。这个方法获取迭代器的所有权并反复调用 next
来遍历迭代器,因而会消费迭代器。在遍历过程中,它将每个项累加到一个总和中,并在迭代完成时返回这个总和。示例 13-13 有一个展示 sum
方法使用的测试:
文件名:src/lib.rs
#[cfg(test)]
mod tests {
#[test]
fn iterator_sum() {
let v1 = vec![1, 2, 3];
let v1_iter = v1.iter();
let total: i32 = v1_iter.sum();
assert_eq!(total, 6);
}
}
调用 sum
之后不再允许使用 v1_iter
因为调用 sum
时它会获取迭代器的所有权。
产生其他迭代器的方法
Iterator
trait 中定义了另一类方法,被称为 迭代器适配器(iterator adaptors),它们不会消耗当前的迭代器,而是通过改变原始迭代器的某些方面来生成不同的迭代器。
示例 13-14 展示了一个调用迭代器适配器方法 map
的例子,该方法使用一个闭包对每个元素进行操作。map
方法返回一个新的迭代器,该迭代器生成经过修改的元素。这里的闭包创建了一个新的迭代器,其中 vector 中的每个元素都被加 1。
文件名:src/main.rs
fn main() { let v1: Vec<i32> = vec![1, 2, 3]; v1.iter().map(|x| x + 1); }
不过这些代码会产生一个警告:
$ cargo run
Compiling iterators v0.1.0 (file:///projects/iterators)
warning: unused `Map` that must be used
--> src/main.rs:4:5
|
4 | v1.iter().map(|x| x + 1);
| ^^^^^^^^^^^^^^^^^^^^^^^^
|
= note: iterators are lazy and do nothing unless consumed
= note: `#[warn(unused_must_use)]` on by default
help: use `let _ = ...` to ignore the resulting value
|
4 | let _ = v1.iter().map(|x| x + 1);
| +++++++
warning: `iterators` (bin "iterators") generated 1 warning
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.47s
Running `target/debug/iterators`
示例 13-14 中的代码实际上并没有做任何事;所指定的闭包从未被调用过。警告提醒了我们原因所在:迭代器适配器是惰性的,因此我们需要在此处消费迭代器。
为了修复这个警告并消费迭代器,我们将使用第十二章示例 12-1 结合 env::args
使用的 collect
方法。这个方法消费迭代器并将结果收集到一个集合数据类型中。
在示例 13-15 中,我们将遍历由 map
调用生成的迭代器结果收集到一个 vector 中。这个 vector 将包含原始 vector 中每个元素加 1 的结果。
文件名:src/main.rs
fn main() { let v1: Vec<i32> = vec![1, 2, 3]; let v2: Vec<_> = v1.iter().map(|x| x + 1).collect(); assert_eq!(v2, vec![2, 3, 4]); }
由于 map
接受一个闭包,因此我们可以指定希望在每个元素上执行的任何操作。这是一个很好的例子,展示了如何通过闭包来自定义某些行为,同时复用 Iterator
trait 提供的迭代行为。
可以链式调用多个迭代器适配器来以一种可读的方式进行复杂的操作。不过因为所有的迭代器都是惰性的,你必须调用一个消费适配器方法,才能从这些迭代器适配器的调用中获取结果。
使用捕获其环境的闭包
很多迭代器适配器接受闭包作为参数,而我们通常会指定捕获其环境的闭包作为迭代器适配器的参数。
作为一个例子,我们使用 filter
方法来获取一个闭包。该闭包从迭代器中获取一项并返回一个 bool
。如果闭包返回 true
,其值将会包含在 filter
提供的新迭代器中。如果闭包返回 false
,其值不会被包含。
示例 13-16 中使用 filter
和一个捕获环境中变量 shoe_size
的闭包来遍历一个 Shoe
结构体集合。它只会返回指定鞋码的鞋子。
文件名:src/lib.rs
#[derive(PartialEq, Debug)]
struct Shoe {
size: u32,
style: String,
}
fn shoes_in_size(shoes: Vec<Shoe>, shoe_size: u32) -> Vec<Shoe> {
shoes.into_iter().filter(|s| s.size == shoe_size).collect()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn filters_by_size() {
let shoes = vec![
Shoe {
size: 10,
style: String::from("sneaker"),
},
Shoe {
size: 13,
style: String::from("sandal"),
},
Shoe {
size: 10,
style: String::from("boot"),
},
];
let in_my_size = shoes_in_size(shoes, 10);
assert_eq!(
in_my_size,
vec![
Shoe {
size: 10,
style: String::from("sneaker")
},
Shoe {
size: 10,
style: String::from("boot")
},
]
);
}
}
shoes_in_size
函数获取一个鞋子 vector 的所有权和一个鞋码作为参数。它返回一个只包含指定鞋码的鞋子的 vector。
shoes_in_size
函数体中调用了 into_iter
来创建一个获取 vector 所有权的迭代器。接着调用 filter
将这个迭代器适配成一个只含有那些闭包返回 true
的元素的新迭代器。
闭包从环境中捕获了 shoe_size
变量并使用其值与每一只鞋的大小作比较,只保留指定鞋码的鞋子。最终,调用 collect
将迭代器适配器返回的值收集进一个 vector 并返回。
这个测试展示当调用 shoes_in_size
时,返回的只会是与我们指定的鞋码相同的鞋子。