参考:https://kaisery.github.io/trpl-zh-cn/ch13-00-functional-features.html
函数式编程 (functional programming )。函数式编程风格通常包含将函数作为参数值或其他函数的返回值、将函数赋值给变量以供之后执行等等。
Rust 的一些在功能上与其他被认为是函数式语言类似的特性,涉及:
- 闭包 (Closures ),一个可以储存在变量里的类似函数的结构
- 迭代器 (Iterators ),一种处理元素序列的方式。作为一个高级的抽象,迭代器被编译成了与手写的底层代码大体一致性能代码。迭代器是 Rust 的 零成本抽象 (zero-cost abstractions )之一,它意味着抽象并不会引入运行时开销
还有其它受函数式风格影响的 Rust 功能,比如模式匹配和枚举,这些已经在其他章节中讲到过了。掌握闭包和迭代器则是编写符合语言风格的高性能 Rust 代码的重要一环,所以我们将专门用一整章来讲解他们。
closure 闭包
自动推断类型
fn add_one_v1 (x: u32) -> u32 { x + 1 } // 一个闭包等价的普通函数的定义
// 闭包的语法
let add_one_v2 = |x: u32| -> u32 { x + 1 }; // 对闭包的参数、返回值强制加上类型注解也是允许的
let add_one_v3 = |x| { x + 1 }; // 类型注解不是必须的,大部分情况下编译器可以推断类型
let add_one_v4 = |x| x + 1 ; // 闭包只有一行时,可以去掉 `{}`
闭包不要求像 fn
函数那样在参数和返回值上注明类型。函数中需要类型注解是因为他们是暴露给用户的显式接口的一部分。严格的定义这些接口对于保证所有人都认同函数使用和返回值的类型来说是很重要的。但是闭包并不用于这样暴露在外的接口:他们储存在变量中并被使用,不用命名他们或暴露给库的用户调用。
闭包通常很短,并只关联于小范围的上下文而非任意情境。强制在这些小的匿名函数中注明类型是很恼人的,并且与编译器已知的信息存在大量的重复。
在这些有限制的上下文中,编译器能可靠的推断参数和返回值的具体类型,类似于它是如何能够推断大部分变量的类型一样。
一旦类型被推断出来,它们就被固定下来,所以如果尝试对同一闭包使用不同类型则会得到类型错误;如果只是定义了闭包,没有调用闭包,也会因为推断不出类型而得到错误。
闭包的作用:
- 作为内联匿名函数来使用,与函数的功能一致,只不过不需要名字。
- “捕获其环境并访问其被定义的作用域的变量”,这是函数不具备的功能。函数作用域之外的变量只能通过参数传递进函数作用域里面,而闭包可以直接访问到闭包外部(上下文)的变量,并且访问的方式涉及三种 trait:
Fn
、FnMut
、FnOnce
(函数也实现了这三种 trait,但是具体功能与闭包是 “不一样的”)。 - 类型匿名。当闭包被定义,编译器会隐式地创建一个匿名类型的结构体,用以储存闭包捕获的 变量,同时为这个未知类型的结构体实现函数功能,通过
Fn
、FnMut
或FnOnce
三种trait
中的一种。 - 利用 trait object,可以将闭包作为函数参数或者返回值类型。
结合泛型和 trait
memoization :记忆缓存,指把昂贵调用开销的函数的返回值缓存起来,单词来源于拉丁语 memorandum —— to be remembered。实质就是缓存,只是作用在函数的返回值上。昂贵调用开销可以指运行时间长、消耗内存大等情况。 lazy evaluation:惰性求值,又称传需求调用 call-by-need,特点是延迟求值+最小化求值,指表达式不会在它被绑定到变量之后就立即求值,而是等用到时再求值。优点是节省内存消耗、可作用在无穷数据结构上(比如求一个很大的 Fibnacci 值)。
对于 memoization,你可以把函数计算的结果直接赋给一个变量,之后不再调用函数(因为其运行成本高),而是使用这个变量。
对于惰性求值,你或许熟知 iterator 类型(在其他语言中也有类似的数据结构),当调用 next 方法时, iterator 才会计算一个值。
在 Rust 中,你除了可以使用结合上面两种方式来同时实现 memoization 和 lazy evaluation,还可以使用 结构体+泛型+trait 这种抽象同时且直观做到这两种技术。观察和思考下面的代码,重点在于 Cacher
结构体和方法实现上:
#[allow(unused)]
#[allow(dead_code)]
use std::{thread, time::Duration, collections::HashMap, hash::Hash};
struct Cacher<T> /* 这里定义了一个泛型结构体 */
where T: Fn(u32) -> u32
/* `Fn` 是一个 trait,任何闭包都实现的 trait,所以泛型 T 就是一个“函数/闭包”类型 */
{
calculation: T, // calculation 是一个“函数/闭包”类型,这是两种技术结合的基础
value: Option<u32>, // 在惰性求值前是没有值的,所以使用 Option 类型再适合不过了,这个设计就是两种技术结合的关键
}
impl<T> Cacher<T>
where T: Fn(u32) -> u32 /* 这里给泛型结构体实现方法,如前诉述,T 是“函数/闭包”类型 */
{
fn new(calculation: T) -> Cacher<T> { // 实例化时只需传入一个闭包
Cacher {
calculation,
value: None, // 实例化时,闭包不求值,所以是 None
}
}
fn value(&mut self, arg: u32) -> u32 { // arg 是闭包内的参数,这个函数的目的是获取闭包实际计算出的值
match self.value {
Some(v) => v, // 有值时(即闭包已经求值了,而且值被存储下来了),则返回这个被缓存的值
None => {
let v = (self.calculation)(arg); // 没有值时,先求值
self.value = Some(v); // 再缓存值
v // 最后依然返回这个被缓存的值
},
}
}
}
fn main() {
// 为了简便,暂时用固定的数代替随机数
let simulated_user_specified_value = 10;
let simulated_random_number = 7;
generate_workout(simulated_user_specified_value, simulated_random_number);
}
// 业务函数:用户输入运动强度,通过一套算法打印出运动计划
fn generate_workout(intensity: u32, random_number: u32) {
let mut expensive_result = Cacher::new(|num| {
println!("calculating slowly...");
thread::sleep(Duration::from_secs(2));
num
});
if intensity < 25 {
println!("Today, do {} pushups!", expensive_result.value(intensity));
println!("Next, do {} situps!", expensive_result.value(intensity)); // lazy evaluation
println!("Next, do {} situps!", expensive_result.value(intensity + 1));
} else {
if random_number == 3 {
println!("Take a break today! Remember to stay hydrated!");
} else {
println!(
"Today, run for {} minutes!",
expensive_result.value(intensity)
);
}
}
}
Cacher
结构体还有一些局限:
虽然实现了 memoization,但是也只会缓存一次值:
value
方法传入任何值,都将是第一次计算的值。我们希望它能实现 第二次开始传入同一值时直接使用已经计算好的值;只有传入未出现的值,才会做一次计算。#[cfg(test)]
mod tests {
use super::*;
#[test]
fn call_with_different_values() {
let mut c = Cacher::new(|a| a);
let v1 = c.value(1);
let v2 = c.value(2);
assert_eq!(v2, 2); // test failed!
}
}
解决方法:尝试修改
Cacher
存放一个哈希 map 而不是单独一个值。哈希 map 的 key 将是传递进来的arg
值,而 value 则是对应 key 调用闭包的结果值。相比之前检查self.value
直接是Some
还是None
值,现在value
函数会在哈希 map 中寻找arg
,如果找到的话就返回其对应的值。如果不存在,Cacher
会调用闭包并将结果值保存在哈希 map 对应arg
值的位置。// 新版本:覆盖掉 Cacher 结构体及其方法
struct Cacher<T>
where
T: Fn(u32) -> u32,
{
calculation: T,
value: Option<u32>, // value 是最后一次求值操作的值,计算过的值都在 stored 里
stored: HashMap<u32, u32>, // 增加存值的 Hashmap<参数, 计算的值>
}
impl<T> Cacher<T>
where
T: Fn(u32) -> u32,
{
fn new(calculation: T) -> Cacher<T> {
Cacher {
calculation,
value: None,
stored: HashMap::new(),
}
}
fn value(&mut self, arg: u32) -> u32 { // 查找是否已计算值
if let Some(v) = self.stored.get(&arg) {
*v
} else {
let v = (self.calculation)(arg);
self.value = Some(v);
self.stored.entry(arg).or_insert(v) // 如果值未计算则插入
}
}
}
只对 u32 类型的参数实现了惰性求值,返回类型的值也是固定的,不够通用:
calculation
的泛型 T 只能传入和返回 u32 类型。
解决方法:引入更多泛型参数。
// 新版本:覆盖掉 Cacher 结构体及其方法
struct Cacher<T, V, K>
where /* 添加 K, V 泛型,把原来是具体类型的地方全部修改掉 */
T: Fn(K) -> V,
V: Clone,
K: Clone + Eq + Hash,
{
calculation: T,
value: Option<V>,
stored: HashMap<K, V>,
}
impl<T, V, K> Cacher<T, V, K>
where /* 相应添加 K, V 泛型,把原来是具体类型的地方全部修改掉 */
T: Fn(K) -> V,
V: Clone,
K: Clone + Eq + Hash,
{
fn new(calculation: T) -> Cacher<T, V, K> {
Cacher {
calculation,
value: None,
stored: HashMap::new(),
}
}
fn value(&mut self, arg: K) -> V {
// 数据在使用过程中经常被传走(move)
// 如果不使用 Clone/Copy trait 就要考虑数据各种移动
if let Some(v) = self.stored.get(&arg) {
v.clone()
} else {
let v = (self.calculation)(arg.clone());
self.value = Some(v.clone());
// entry 方法需要用到 Eq + Hash
self.stored.entry(arg).or_insert(v).clone()
}
}
}
测试用例:
#[test]
fn call_and_return_with_different_types() {
let mut c = Cacher::new(|a: u32| -> i32 { a as i32 });
let v = c.value(1 as u32);
assert_eq!(v, 1);
let mut c = Cacher::new(|a: i32| -> String { a.to_string() });
let v = c.value(0);
assert_eq!(v, 0.to_string());
}
捕获上下文变量
fn main() {
let x = 4;
let y = 4;
let equal_to_x = |z| z == x; // x 来自于闭包之外,而且不作为闭包的参数
assert!(equal_to_x(y)); // 成功捕获到 x,而且是以 `Fn` trait 方式,即 x 被 borrow (不可变借用)
fn equal_to_x(z: i32) -> bool { z == x } // 闭包转化成一般函数
assert!(equal_to_x(y)); // 报错,因为函数不支持获取外部变量
}
感受闭包在无需类型说明就能在大多数时候处理捕获的环境变量:
use std::mem;
fn main() {
let color = "green";
// 这个闭包打印 `color`。它会立即借用(通过引用,`&`)`color` 并将该借用和
// 闭包本身存储到 `print` 变量中。`color` 会一直保持被借用状态直到
// `print` 离开作用域。
// `println!` 只需传引用就能使用,而这个闭包捕获的也是变量的引用,因此无需
// 进一步处理就可以使用 `println!`。
let print = || println!("`color`: {}", color);
// 调用闭包,闭包又借用 `color`。
print();
print();
let mut count = 0;
// 这个闭包使 `count` 值增加。要做到这点,它需要得到 `&mut count` 或者
// `count` 本身,但 `&mut count` 的要求没那么严格,所以我们采取这种方式。
// 该闭包立即借用 `count`。
//
// `inc` 前面需要加上 `mut`,因为闭包里存储着一个 `&mut` 变量。调用闭包时,
// 该变量的变化就意味着闭包内部发生了变化。因此闭包需要是可变的。
let mut inc = || {
count += 1;
println!("`count`: {}", count);
};
// 调用闭包。
inc();
inc();
//let reborrow = &mut count;
// ^ 试一试:将此行注释去掉。
// 不可复制类型(non-copy type)。
let movable = Box::new(3);
// `mem::drop` 要求 `T` 类型本身,所以闭包将会捕获变量的值。这种情况下,
// 可复制类型将会复制给闭包,从而原始值不受影响。不可复制类型必须移动
// (move)到闭包中,因而 `movable` 变量在这里立即移动到了闭包中。
let consume = || {
println!("`movable`: {:?}", movable);
mem::drop(movable);
};
// `consume` 消耗了该变量,所以该闭包只能调用一次。
consume();
//consume();
// ^ 试一试:将此行注释去掉。
}
闭包可以通过三种方式捕获其环境(闭包周围的作用域被称为其 环境 ,environment),他们直接对应函数的三种获取参数的方式:获取所有权,可变借用和不可变借用。
当以闭包作为输入参数时,必须指出闭包的完整类型,它是通过使用以下三个 Fn
trait 中的一种来指定的:
FnOnce
消耗(consume 指使用并且使用后让变量消失)从周围作用域捕获的变量。为了消耗捕获到的变量,闭包必须获取其所有权并在定义闭包时将其移动进闭包。其名称的Once
部分代表了闭包不能多次获取相同变量的所有权的事实,所以它只能被调用一次。FnMut
获取可变的借用值所以可以改变其环境Fn
从其环境获取不可变的借用值
当创建一个闭包时,Rust 根据其如何使用环境中变量来推断我们希望如何引用环境:
- 由于所有闭包都可以被调用至少一次,所以所有闭包都实现了
FnOnce
; - 那些并没有移动被捕获变量的所有权到闭包内的闭包也实现了
FnMut
; - 而不需要对被捕获的变量进行可变访问的闭包则也实现了
Fn
。
大部分需要指定一个 Fn
系列 trait bound 的时候,可以从 Fn
开始,而编译器会根据闭包体中的情况告诉你是否需要 FnMut
或 FnOnce
。
而实际的情况也可能是这样的:
例如用一个类型说明为 FnOnce
的闭包作为参数。这说明闭包可能采取 &T
,&mut T
或 T
中的一种捕获方式,但编译器最终是根据所捕获变量在闭包里的实际使用情况决定捕获 方式。
这是因为如果能以移动的方式捕获变量,则闭包也有能力使用其他方式借用变量。
然而反过来就不再成立:如果参数的类型说明是 Fn
,那么不允许该闭包通过 &mut T
或 T
捕获变量。
这是一个例子:
// 该函数将闭包作为参数并调用它。
fn apply<F>(f: F)
where F: FnOnce() {
// ^ 试一试:将 `FnOnce` 换成 `Fn` 或 `FnMut`
// 传入的 farewell 最终需要数据的所有权,并把值 drop 掉,
// 所以 `Fn` 或 `FnMut` 都无法做到
f();
}
// 输入闭包,返回一个 `i32` 整型的函数。
fn apply_to_3<F>(f: F) -> i32
where F: Fn(i32) -> i32 {
// ^ 试一试:将 `Fn` 换成 `FnMut` 或 `FnOnce`
// 传入的 diary 没有更改或者占有数据,因此可以使用这三种 trait
f(3)
}
fn main() {
use std::mem;
let greeting = "hello"; // greeting: &str
// 不可复制的类型。
// `to_owned` 从借用的数据创建有所有权的数据。
let mut farewell = "goodbye".to_owned(); // mut farewell: String
// 捕获 2 个变量:通过引用捕获 `greeting`,通过值捕获 `farewell`。
let diary = || { // diary: || -> ()
// `greeting` 通过引用捕获,故需要闭包是 `Fn`。
println!("I said `{}`.", greeting);
// 下文改变了 `farewell` ,因而要求闭包通过可变引用来捕获它。
// 现在需要 `FnMut`。
farewell.push_str("!!!");
println!("Then I screamed `{}`.", farewell);
println!("Now I can sleep. zzzzz");
// 手动调用 drop 又要求闭包通过值获取 `farewell`。
// 现在需要 `FnOnce`。
mem::drop(farewell);
};
// 以闭包作为参数,调用函数 `apply`。
apply(diary);
// 闭包 `double` 满足 `apply_to_3` 的 trait 约束。
let double = |x| 2 * x; // double: |…| -> i32 // x: i32
println!("3 doubled: {}", apply_to_3(double));
}
如果你希望强制闭包获取其使用的环境值的所有权,可以在参数列表前使用
move
关键字。这个技巧在将闭包传递给新线程以便将数据移动到新线程中时最为实用。fn main() {
let x = vec![1, 2, 3];
// 强制让闭包获取 x 的所有权,x 被移入闭包(会在闭包内一直存在)
let equal_to_x = move |z| z == x;
// x 不再被使用
//println!("can't use x here: {:?}", x);
let y = vec![1, 2, 3];
assert!(equal_to_x(y)); // 代码正常运行
}
延伸阅读:https://huonw.github.io/blog/2015/05/finding-closure-in-rust/
作为输入/输出参数
闭包作为输入参数是可能的,因此返回闭包作为输出参数也是可能的。但是,根据定义,匿名闭包类型是未知的,因此我们必须使用它
impl FnTrait
来返回它们。这里的 FnTrait 就是前面三种:Fn
、FnMut
、FnOnce
(以前FnOnce
并不直接支持,需要FnBox
类型,1.53 版本之后 不需要了)
除此之外,如果使用到了函数内的环境变量,则必须使用move
关键字,它表明所有的捕获都是通过值进行的。因为在函数退出时,任何通过引用的捕获都被丢弃,在闭包中留下无效的引用。fn create_fn() -> impl Fn() {
let text = "Fn".to_owned();
move || println!("This is a: {}", text)
}
fn create_fnmut() -> impl FnMut() {
let text = "FnMut".to_owned();
move || println!("This is a: {}", text)
}
fn create_fnonce() -> impl FnOnce() {
let text = "FnOnce".to_owned();
move || println!("This is a: {}", text)
}
fn main() {
let fn_plain = create_fn();
let mut fn_mut = create_fnmut();
let fn_once = create_fnonce();
fn_plain();
fn_mut();
fn_once();
}
这里的
impl FnTrait()
不可以写成 trait bound 语法,因为编译器无法返回一个泛型类型,因为它的大小未知:// 无效
fn create_fnonce<F>() -> F
where F: FnOnce() { }
// 无效
fn create_fnonce<F: FnOnce()>() -> F { }
workaround 是用
Box
封装一个大小已知(实际是指针大小)的类型:fn create_fnonce() -> Box<dyn FnOnce()> {
let text = "FnOnce".to_owned();
Box::new(move || println!("This is a: {}", text))
}
所以远不如
impl FnTrait()
语法方便。
这个impl Trait
“作为函数返回值” 的语法可以拓展到任意类型,但是正如上面处理的那样,返回值大小是个指针大小,所以类比引用的生命周期,返回同一 trait 的不同具体类型时,是无法通过编译的,比如 “这个例子” 。
Rust Book “最后一个实战例子” 用到了这样一个类型:
type Job = Box<dyn FnOnce() + Send + 'static>;
这在早期(v1.53 前)的 Rust 版本中无法直接实现,而是需要用 Box
封装一个 trait object 和 call_box 函数:
trait FnBox {
fn call_box(self: Box<Self>);
}
impl<F: FnOnce()> FnBox for F {
fn call_box(self: Box<F>) {
(*self)()
}
}
type Job = Box<dyn FnBox + Send + 'static>;
iterator
迭代器模式允许你对一个序列的项进行某些处理。迭代器 (iterator )负责遍历序列中的每一项和决定序列何时结束的逻辑。
在 Rust 中,迭代器是 惰性的 (lazy ),这意味着在调用方法使用迭代器之前它都不会有效果。
Iterator trait 与 next
方法
迭代器都实现了一个叫做 Iterator
的定义于标准库的 trait。这个 trait 的定义看起来像这样:
pub trait Iterator {
// 表明需要定义关联类型 (associated type)
type Item;
// next 是 Iterator 实现者被要求自己定义的唯一 method,要求实例是一个可变引用
// next 一次返回迭代器中的一个 Item,封装在 Some 中,当迭代器结束时,它返回 None。
fn next(&mut self) -> Option<Self::Item>; // Self 指 Iterator
// 此处省略方法的默认实现(因为 impl trait 给某个数据结构时不需要自己实现)
}
// self 用在定义方法时的第一个参数,由于方法作用在实例上,所以可以把 self 看作实例
// self 也可以用在引入作用域上,如 `use std::{self, fs}`
// Self 指代当前 `impl 类型对象` 块中的类型对象/trait,把 Self 看作抽象的数据类型,而不是实例
self => self: Self
&self => self: &Self
&mut self => self: &mut Self
实际在使用时:
fn main() {
let v1 = vec![1, 2, 3];
// 创建迭代器,iter 是定义在 Vec 上的方法
let mut v1_iter = v1.iter();
// v1_iter 需要是可变的,因为在迭代器上调用 next 方法,改变了迭代器中用来记录序列位置的状态
// 从 next 调用中得到的值是 vector 的不可变引用
// 换句话说,代码 消耗(consume)或使用了迭代器
// 每一个 next 调用都会从迭代器中消耗一个项(但是 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);
// 使用 for 循环时无需使 v1_iter 可变,因为 for 会隐式调用 .into_iter() 方法
// 即 for 循环会获取 v1_iter 的所有权并在后台使 v1_iter 可变
for val in v1_iter {
println!("Got: {}", val);
}
// 无法再次使用 v1_iter这个变量了,因为在 for 结构中被获取所有权,且被消耗掉了
// v1_iter 与 v 是没有所有权关系的,所以 v 依然可以被使用
}
iterator 关于所有权/引用的三种方法:
iter
:生成其项是不可变引用的迭代器。into_iter
:生成其项和自身是具有所有权的迭代器。像从原来的数据创建 iterator 那样—— iterator 的数据来自原数据,但自身是新的独立变量。iter_mut
:生成其项是可变引用的迭代器。consuming adaptors
消耗转换器 (consuming adaptors ):指 调用next
方法的、会消耗掉迭代器的一类 method(方法)。调用 consuming adaptors 时,它们会消耗迭代器,即里面的项及迭代器自身(next 方法不会消耗迭代器自身,因为它消耗的是里面的项),所以它们会获取迭代器的所有权,而不仅仅是可变引用。个人理解: adaptor 如果翻译成“适配器”就很生硬,因为 adapt 表示“适应”,指通过调整自己来顺应环境变化,所以用在这里的情境中 adapt 强调 iterator 自身发生了改变。 consuming 指明了这种改变是消耗层面的。 iterator 从一个拥有所有权的“自由”变量,在调用某些方法后,转变成了“虚无”(原 iterator 不存在)和“新生”(方法返回的新的结果)。
一些方法在其定义中调用了 next
方法,这也就是为什么在实现 Iterator
trait 时要求实现 next
方法的原因。
fn main() {
let v1 = vec![1, 2, 3];
// 没有显示调用 next,所以不需要 mut borrow,而后面的 sum 会直接获取所有权
let v1_iter = v1.iter();
// `sum` 方法获取迭代器的所有权并反复调用 `next` 来遍历迭代器,因而会消费迭代器
// 当其遍历每一个项时,它将每一个项加总到一个总和并在迭代完成时返回总和
let total: i32 = v1_iter.sum();
assert_eq!(total, 6);
// v1_iter 因为 sum 方法被获取了所有权、被消耗掉了,无法再使用 v1_iter
}
迭代器被消耗的概念:
fn main() {
let v1 = vec![1, 2, 3];
// v1_iter 需要是可变的,因为调用了 next 方法,改变了迭代器中用来记录序列位置的状态
let mut v1_iter = v1.iter();
// 消耗第一个项
println!("{:?}", v1_iter.next()); // Some(1)
// v1_iter 只剩下两个元素
let total: i32 = v1_iter.sum();
assert_eq!(total, 5);
// v1_iter 被 sum 消耗掉,无法再使用
}
iterator adaptors
迭代器转换器 (iterator adaptors ):Iterator
trait 中定义了的另一类方法,它们将当前迭代器变为不同类型的迭代器。可以链式调用多个迭代器适配器。不过因为所有的迭代器都是惰性的,必须调用一个消耗转换器方法 (consuming adaptor) 以便获取迭代器转换器 (iterator adaptor) 调用的结果。map
方法就是典型的 iterator adaptor,传入闭包来调用每个元素以生成新的迭代器。
let v1: Vec<i32> = vec![1, 2, 3];
v1.iter().map(|x| x + 1);
// 得到 warning: unused `std::iter::Map` which must be used:
// iterator adaptors are lazy and do nothing unless consumed
collect
方法是一个常用的 consuming adaptor,它将迭代器的结果收集到一个数据结构中:
let v1: Vec<i32> = vec![1, 2, 3];
// 调用 map 方法创建一个新迭代器
// 接着调用 collect 方法消费新迭代器并创建一个 vector
let v2: Vec<_> = v1.iter().map(|x| x + 1).collect();
assert_eq!(v2, vec![2, 3, 4]);
// Result<T, E> 的迭代器实现有些特殊,如果一个 Result 序列里面存在 Err 类型,那么 collection 会返回第一个 Err
// 如果序列里面都是 Ok,则 collection 可以全部返回
fn main() {
let results = [Ok(1), Err("nope"), Ok(3), Err("bad")];
let result: Result<Vec<_>, &str> = results.iter().cloned().collect();
// gives us the first error
assert_eq!(Err("nope"), result);
let results = [Ok(1), Ok(3)];
let result: Result<Vec<_>, &str> = results.iter().cloned().collect();
// gives us the list of answers
assert_eq!(Ok(vec![1, 3]), result);
}
结合 closure 获取上下文变量
此处展示使用 filter
iterator adaptor 和捕获环境的闭包的常规用例。
迭代器的 filter
方法获取一个使用迭代器的每一个项并返回布尔值的闭包。如果闭包返回 true
,其值将会包含在 filter
提供的新迭代器中。如果闭包返回 false
,其值不会包含在结果迭代器中。
#[derive(PartialEq, Debug)]
struct Shoe {
size: u32,
style: String,
}
fn shoes_in_my_size(shoes: Vec<Shoe>, shoe_size: u32) -> Vec<Shoe> {
// iter 方法获取不可变引用数据,迭代器里面是引用类型的数据,链式迭代就是访问和操作引用
// 而我们需要的 Vec 元素为 Shoe 这样具有所有权的数据(而不需要引用),所以使用 into_iter
shoes.into_iter().filter(|s| s.size == shoe_size).collect() // shoe_size 来自于闭包外
}
fn main() {
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_my_size(shoes, 10);
assert_eq!(
in_my_size,
vec![
Shoe {
size: 10,
style: String::from("sneaker")
},
Shoe {
size: 10,
style: String::from("boot")
},
]
); // 程序未报错
}
创建自定义迭代器
Iterator
trait “只有 next 方法需要自己提供实现” ,一旦定义了 next 方法,就可以使用所有其他由 Iterator
trait 提供的拥有默认实现的方法来创建自定义迭代器了。
// 定义结构体
struct Counter {
// Counter 结构体有一个字段 count,存放一个 u32 值
// 它会记录处理 1 到 5 的迭代过程中的位置
count: u32,
}
// 定义关联函数,用于创建实例
impl Counter {
fn new() -> Counter { Counter { count: 0 } } // count 是私有的,由 iterator 控制
}
impl Iterator for Counter {
type Item = u32;
fn next(&mut self) -> Option<Self::Item> {
// 将迭代器的关联类型 Item 设置为 u32,意味着迭代器会返回 u32 值集合
self.count += 1;
// 迭代器对其内部状态加一
// 如果 count 值小于 6,next 会返回封装在 Some 中的当前值
// 如果 count 大于或等于 6,迭代器会返回 None
if self.count < 6 {
Some(self.count)
} else {
None
}
}
}
fn main() {
let mut counter = Counter::new();
assert_eq!(counter.next(), Some(1));
assert_eq!(counter.next(), Some(2));
assert_eq!(counter.next(), Some(3));
assert_eq!(counter.next(), Some(4));
assert_eq!(counter.next(), Some(5));
assert_eq!(counter.next(), None);
let sum: u32 = Counter::new()
.zip(Counter::new().skip(1)) // zip 把两个迭代器组合在一起,形成新的配对的迭代器
.map(|(a, b)| a * b) // 每一对值相乘 => (2, 6, 12, 20)
.filter(|x| x % 3 == 0) // 3 的倍数,所以只剩下 (6, 12)
.sum(); // consuming adaptor 执行计算并求和
assert_eq!(18, sum);
}
skip(n)
方法表示忽略迭代器前 n 个元素,所以zip
只产生四对值,理论上第五对值(5, None)
从未被产生,因为 zip
在任一输入迭代器返回 None
时也返回 None
。