13函数式语言特性:迭代器和闭包

函数式(functional programming)编程风格通常包含将函数作为参数值或其他函数的返回值、将函数赋值给变量以供之后执行等。

Rust 上的函数式语言功能:

  • 闭包(Closures),一个可以存储在变量里的类似函数的结构;
  • 迭代器(Iterators),一种处理元素序列的方式;

闭包:可以捕获其环境的匿名函数

闭包(Closures),是可以将作为值赋值给变量,或作为参数传递给其他函数的匿名函数

  • 可以在一个地方创建闭包,然后在不同的上下文中执行闭包运算。

  • 不同于函数,闭包允许捕获调用者作用域中的值。

    直接使用其所在位置上下文中定义的变量,无需将这些外部变量作为参数传入。

1
2
3
4
5
6
let color = String::from("green");
// 这里直接捕获外部变量 color。同时这里是通过不可变借用的方式捕获。不影响 color 在后面的使用。
let print = || println!("`color`: {}", color);
// Call the closure using the borrow.
print();
let _reborrow = &color;

使用闭包创建行为的抽象

闭包的行为特点:

  • 允许我们在一个地方创建闭包,然后再程序的指定位置需要结果的时候才执行这些代码
    • 与函数的区别,函数会直接调用,然后将计算结果返回给变量,而闭包是将整体作为参数赋值给变量,在后面调用该变量时才执行。即将整体压进栈中,而不是将返回值压进栈中。

例子,调用函数

1
2
3
4
5
6
7
8
9
10
11
12
fn generate_workout(intensity: u32, random_number: u32) {
// 这里调用函数,然后将函数结果返回赋值给变量,后面直接使用变量值。
// 缺点是不管后面用不用都会先调用函数
let expensive_result = simulated_expensive_calculation(intensity);
if intensity < 25 {
println!("Today, do {} pushups!",expensive_result);
} else {
println!("");
}
}

fn simulated_expensive_calculation(intensity: u32) -> u32 {

例子,创建闭包

1
2
3
4
5
let expensive_closure = |num| {
println!("calculating slowly...");
thread::sleep(Duration::from_secs(2));
num
};

闭包定义:

  1. 闭包的一定以一对竖线(|)开始,在竖线中指定闭包的参数,多个参数间,使用逗号(,)分隔;(与Ruby的闭包定义类似)
    • |param1, param2|
  2. 参数后,是存放闭包体的大括号(如果比包体只有一行,可以省略大括号);
  3. 闭包体内,与函数体一样,最后一行没有分号,默认作为返回值;

闭包调用:

  • 调用闭包类似于调用函数,
  • 指定存放闭包定义的变量名,并在后面跟包含要使用的参数的括号;
  • 存放闭包定义的变量名(参数);
1
2
3
4
5
6
7
8
9
10
// 这里表示将闭包的定义赋值给变量
let expensive_closure = |num| {
println!("calculating slowly...");
thread::sleep(Duration::from_secs(2));
num
};

// 这是闭包的调用,
// 存储闭包定义的变量名(参数)
expensive_closure(5);

闭包类型推断和注解

闭包不要求 fn函数那样显示注明参数和返回值的类型。

  • 函数需要暴露给用户,所以严格的定义有利于保证正确的参数和返回值的类型;
  • 闭包不对外暴露,同时上下文比较小,编译器能可靠的推断参数和返回值的类型;
1
2
3
4
5
// 以下都是合法定义
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 ;

闭包定义会为每个参数和返回值推断一个唯一具体类型。

1
2
3
4
5
let example_closure = |x| x;

// 只能有一种类型
let s = example_closure(String::from("hello")); --ok
let n = example_closure(5); --wrong

使用带有泛型和Fn trait 的闭包

闭包还可以存放在结构体,枚举或函数参数中

  • 需要指定闭包的类型
  • 需要使用泛型和 trait bound

注:每个闭包实例有其独有的匿名类型。

  • 即便两个闭包有相同的签名,它们的类型仍然可以被认为是不同的。

Fn 系列 trait 之间的区别

Fn 系列 trait 由标准库提供。所有的闭包都实现了 Fn、FnMut、FnOnce trait 中的一个。

  • Fn trait,从其环境中获取不可变借用;
  • FnMut trait,获取可变借用值,所以可以改变其环境值;
  • FnOnce trait,获取其所有权并在定义闭包时将其移动进闭包。
    • 从名字中Once 部分也可以说明不能多次获取相同变量的所有权,所以只能是Once

当创建闭包时,Rust 编译器会根据其如何使用环境中的变量来推断如何实现 Fn系列 trait。

三个 Fn 系列 trait 的关系可以简单理解:(更深层次的理解有些复杂,放在以后处理)

  • 实现了 Fn 的一定实现了 FnMut
  • 实现了 FnMut 的一定实现了 FnOnce

所有闭包都可以被调用至少一次,所以所有闭包都实现了FnOnce

没有移动捕获的环境值所有权的闭包也实现了FnMut

不需要对捕获的变量进行可变借用访问的闭包则实现了Fn

实际使用时

  • 当需要指定一个 Fn 系列 trait bound 的时候,可以从Fn 开始,然后编译器会根据闭包体中的情况告诉你是否需要 FnMutFnOnce

注意:函数也都实现了这三个 Fn trait。如果不需要捕获环境中的值,则可以使用实现了 Fn trait 的函数而不是闭包。

闭包会捕获其环境

对于上面提到的闭包捕获其环境,

就是闭包可以使用其上下文中定义的变量。(闭包周围的作用域被称为其环境environment)

  • 函数不行,函数不能使用没有在函数签名或函数体中定义的变量。
    • 闭包捕获其环境的能力,需要使用内存并产生额外开销,
    • 函数不允许捕获其环境值,所以定义和使用函数也就不会有这些额外开销。
1
2
3
4
5
6
7
8
9
10
11
fn main() {
let x = 4;

let equal_to_x = |z| z == x; // 闭包这样用可以

// fn equal_to_x(z: i32) -> bool { z == x } --这样用会报错

let y = 4;

assert!(equal_to_x(y));
}

闭包可以通过三种方式捕获其环境,对应函数的三种获取参数的方法:三种不同的 trait

  • 获取所有权 FnOnce;(在[上面](# 系列 trait 之间的区别)已经解释过三者之间的区别)
  • 获取可变借用 FnMut
  • 获取不可变借用 Fn

三者的使用范围:

如果一个参数使用FnOnce 标记,那么&T&mut TT三种形式闭包都可以捕获。

反过来,如果一个参数使用Fn 标记,那么只能捕获&T这种形式。

如何强制闭包获取其使用的环境值的所有权?

  • 可以在参数列表前使用 move 关键字。
  • 使用场景:在将闭包传递给新线程以便将数据移动到新线程中时最为实用。
    • 第十六章讨论并发时会展示更多 move 闭包的例子。
1
2
3
4
5
6
7
8
9
10
11
fn main() {
let x = vec![1, 2, 3];

let equal_to_x = move |z| z == x; // ok,x 已经移动到闭包中了。

println!("can't use x here: {:?}", x); // 编译器报错,x 的所有权在上面已经发生了移动,且 vec 没有实现 Copy trait。

let y = vec![1, 2, 3];

assert!(equal_to_x(y));
}

作为输入参数

因为闭包是匿名类型(type anonymity),所以闭包作为参数时,需要使用**泛型+trait bounds** 的形式。

  • 其中 trait bounds 一般是 FnFnMutFnOnce 这三个trait。

例如:

1
2
3
4
5
6
7
8
9
10
11
// `F` must be generic.
fn apply<F>(f: F)
where
F: FnOnce(),
{
f();
}

fn call_me<F: Fn()>(f: F) {
f();
}

闭包作为输入参数时,闭包必须使用FnFnMutFnOnce 这三个trait之一来显示标注。

这三个 trait 的限制从多到少依次是:

  • Fn:闭包通过引用的方式捕获(&T);

  • FnMut:闭包通过可变引用的方式捕获(&mut T);

  • FnOnce:闭包通过值捕获(T,通过值,也就是通过获取所有权的方式);

在逐个变量的基础上,编译器将以尽可能少的限制方式捕获变量。

当定义闭包时,编译器会隐式创建一个新的匿名结构体来存储被捕获的变量。同时将定义时FnFnMutFnOnce`其中用到的 trait 作为这个变量的类型,直到被调用。

作为输出参数

根据定义,匿名闭包类型是未知的,所以我们必须使用impl Trait来返回它们。

除此之外,必须使用move关键字,这表示所有捕获都是按值进行的(需要获取所有权)。这是必需的,因为任何通过引用捕获的内容都会在函数退出时被删除,在闭包中留下无效的引用。

  • 防止出现无效引用。

使用迭代器处理元素序列

迭代器模式,允许你对一个项的序列进行某些处理。

迭代器(iterator),负责遍历序列中的每一项和决定序列何时结束的逻辑。

在 Rust 中,迭代器是惰性的(lazy),

  • 即,仅创建迭代器,没有调用执行方法,迭代器是不工作的。
1
2
3
let v1 = vec![1, 2, 3];

let v1_iter = v1.iter(); // 创建一个迭代器。不过这段代码没有任何作用,因为不会执行。

迭代器的实现方式,提供了对多种不同的序列使用相同逻辑的灵活性。

Iterator trait 和 next 方法

迭代器都实现了一个叫做 Iterator 的 trait,定义在标准库中。

1
2
3
4
5
6
7
pub trait Iterator {
type Item;

fn next(&mut self) -> Option<Self::Item>;

// 此处省略了方法的默认实现
}
  • type ItemSelf::Item,它们定义了 trait 的 关联类型(associated type),在 19章高级特性 中会深入讲解。
    • 现在简单知道这表明实现 Iterator trait 要求同时定义一个 Item ,这个 Item 类型被用作 next 方法的返回值类型。换句话说,Item 类型将是迭代器返回元素的类型。
  • nextIterator 实现者被要求定义的唯一方法。next 一次返回迭代器中的一个项,封装在 Some 中,当迭代器结束时,它返回 None

在迭代器上调用 next 方法改变了迭代器中用来记录序列位置的状态

  • 即,代码消费(consume)了,或使用了迭代器(每个 next 调用都会从迭代器中消费一个项)。

注意:使用 for 循环时无需使迭代器可变(mut),因为 for 循环会获取迭代器的所有权并在后台使用迭代器可变。

关于迭代器和所有权:

注意:从next 调用中得到的值是不可变引用。iter()方法生成一个不可变引用的迭代器。

into_iter 可以获取所有权并返回拥有所有权的迭代器;

iter_mut 可以获取可变引用的迭代器。

消费迭代器的方法

Iterator trait 有一系列不同的由标准库提供的默认实现方法。通过标准库API文档查找。

一些方法在其定义中调用了next 方法,这也是为什么在实现 Iterator trait 时要求实现 next 方法的原因。

  • 它们被称为 消费适配器(consuming adaptors),因为调用它们会消耗迭代器。
  • 例如,sum

产生其他迭代器的方法

迭代器适配器(iterator adaptors) ,允许我们将当前迭代器转变为不同类型的迭代器。可以链式调用多个迭代器适配器。

  • 所有迭代器都是惰性的。必须调用一个消费适配器方法才能获取结果。

迭代器适配器和消费适配器,有些像 spark 中的 转换(transformation)和行动(action),转换用于在已有的数据集上生成新的数据集,行动用于产生结果集。

1
2
3
4
5
6
7
let v1: Vec<i32> = vec![1, 2, 3];

// 为什么这里 x 的类型为 &i32,但是却可以进行 &i32 + 1?
// 因为标准库实现了 &i32 + i32的操作。
let v2: Vec<_> = v1.iter().map(|x| x + 1).collect();

assert_eq!(v2, vec![2, 3, 4]);

实现 Iterator trait 来创建自定义迭代器

可以为标准库中其他的集合类型创建迭代器。

同时因为定义中唯一要求实现的方法next 方法,所以可以通过实现它来创建自定义迭代器。

使用自定义迭代器中其他 Iterator trait 方法

a % b ,如果 b 是2 的n次幂,那么就有 a & (b -1)

zip 方法在任一输入迭代器返回 None 时也返回 None,所以如果两组迭代组合不可能产生类似 (5,None) 这样的组合。

改进之前的 I/O 项目

使用迭代器并去掉 clone

函数式编程风格倾向于最小化可变状态的数量来使代码更简洁。

去掉可变状态可能会使得将来进行并行搜索的增强变得更容易,因为我们不必管理 results vector 的并发访问

性能比较:循环对迭代器

闭包和迭代器的实现,达到了不影响运行时性能的程序。

这是Rust 竭力提供零成本抽象的目标的一部分。

  • 版权声明: 本博客所有文章除特别声明外,著作权归作者所有。转载请注明出处!
  • Copyrights © 2022-2023 ligongzhao
  • 访问人数: | 浏览次数:

请我喝杯咖啡吧~

支付宝
微信