闭包

闭包:可以捕获其环境的匿名函数.Rust的闭包是匿名函数,可以保存在变量中,也可以作为参数传递给其他函数。您可以在一个位置创建闭包,然后调用闭包以在不同的上下文中对其进行评估。函数和闭包都是实现了FnFnMutFnOnce特质(trait)的类型。任何实现了这三种特质其中一种的类型的对象,都是 可调用对象 ,都能像函数和闭包一样通过这样name()的形式调用,()在rust中是一个操作符,操作符在rust中是可以重载的。rust的操作符重载是通过实现相应的trait来实现,而()操作符的相应trait就是FnFnMutFnOnce,所以,任何实现了这三个trait中的一种的类型,其实就是重载了()操作符。Rust 將函數和 Closure 視為不同的東西,函數不是表達式,而 Closure 是。编译器倾向于通过不可变的借入捕获一个闭包变量,随后是可变借入,通过复制,最后通过移动。它会选择这些允许闭包进行编译的首选。如果使用move关键字,则无论借用是否有效,所有捕获都通过移动或复制进行.

闭包可以通过三种方式从其环境中捕获值,这直接映射到函数可以采用参数的三种方式:获取所有权,可变借入和不可变借入。

  • 所有闭包都实现了FnOnce,它们都可以通过消耗闭包的所有权被调用至少一次
  • 不移动捕获变量的闭包实现FnMut,可以通过可变引用调用,它可变地借用了值。
  • 不需要对捕获变量进行可变访问的闭包实现Fn,可以通过共享引用调用
  • 要强制闭包取得它在环境中使用的值的所有权,可以在参数列表之前使用move关键字。 当将闭包传递给新线程以move数据以使其由新线程拥有时,此技术非常有用。

注意:移动闭包仍然可以实现[Fn]或[FnMut],即使它们通过移动捕获变量。 这是因为闭包类型实现的特征取决于闭包对捕获值的作用,而不是捕获它们的方式。

所有闭包类型都实现[Sized]。 此外,如果通过它存储的捕获类型允许这样做,闭包类型可以实现以下特征:

  • [Clone]
  • [Copy]
  • [Sync]
  • [Send]

[Send]和[Sync]匹配普通结构体的规则,而[Clone]和[Copy]的行为就像派生一样。 对于[Clone],未指定克隆捕获变量的顺序。

The rules for Send and Sync match those for normal struct types, while Clone and Copy behave as if derived. For Clone, the order of cloning of the captured variables is left unspecified.

由于捕获通常是通过引用,因此出现以下一般规则:

  • 如果通过可变引用复制移动捕获的所有变量都是[Sync],则闭包是[Sync]。
  • 如果共享引用捕获的所有变量都是[Sync],则闭包为[Send],并且可变引用复制移动捕获的所有值均为[Send]。
  • 闭包是[Clone]或[Copy],如果它没有通过可变引用捕获任何值,并且如果它通过复制移动捕获的所有值分别是[Clone]或[Copy]。

语法

  1. let plus_one = |x| x + 1;
  2. assert_eq!(2, plus_one(1));

Rust 中,Closure 的类型被视为一种trait,和其他的 trait 一樣,本身不能实例化,借助 Box<T> 能將其实例化。为解決所有权问题,Rust 使用 move 关键字將变数的所有权移到函式外。

  1. fn add_one(x: i32) -> Box<Fn(i32) -> i32> {
  2. Box::new(move |n| n + x)
  3. }
  4. fn main() {
  5. let f = add_one(5);
  6. assert_eq!(6, f(1));
  7. }

闭包及环境

之所以把它称为“闭包”是因为它们“包含在环境中”(close over their environment)。这看起来像:

  1. let num = 5;
  2. let plus_num = |x| x + num;
  3. assert_eq!(10, plus_num(5));

这个闭包,plus_num,引用了它作用域中的let绑定:num。更明确的说,它借用了绑定。如果我们做一些会与这个绑定冲突的事,我们会得到一个错误。

move闭包

我们可以使用move关键字强制使我们的闭包取得它环境的所有权, move关键字通常用于允许闭包比捕获的值生命周期更长,例如,如果返回闭包或用于生成新线程。

  1. let num = 5;
  2. let owns_num = move |x: i32| x + num;

现在,即便关键字是move,变量遵循正常的移动语义。在这个例子中,5实现了Copy,所以owns_num取得一个5的拷贝的所有权。那么区别是什么呢?

  1. let mut num = 5;
  2. {
  3. let mut add_num = |x: i32| num += x;
  4. add_num(5);
  5. }
  6. assert_eq!(10, num);

那么在这个例子中,我们的闭包取得了一个num的可变引用,然后接着我们调用了add_num,它改变了其中的值,正如我们期望的。我们也需要将add_num声明为mut,因为我们会改变它的环境。

如果我们改为一个move闭包,这有些不同:

  1. let mut num = 5;
  2. {
  3. let mut add_num = move |x: i32| num += x;
  4. add_num(5);
  5. }
  6. assert_eq!(5, num);

我们只会得到5。与其获取一个我们num的可变借用,我们取得了一个拷贝的所有权。

另一个理解move闭包的方法:它给出了一个拥有自己栈帧的闭包。没有move,一个闭包可能会绑定在创建它的栈帧上,而move闭包则是独立的。例如,这意味着大体上你不能从函数返回一个非move闭包。

不过在我们讨论获取或返回闭包之前,我们应该更多的了解一下闭包实现的方法。作为一个系统语言,Rust给予你了大量的控制你代码的能力,而闭包也是一样。

闭包实现

Rust 的闭包实现与其它语言有些许不同。它们实际上是trait的语法糖。在这以前你会希望阅读trait。我们使用trait系统来重载运算符。调用函数也不例外。我们有三个trait来分别重载:

  1. mod foo {
  2. pub trait Fn<Args> : FnMut<Args> {
  3. extern "rust-call" fn call(&self, args: Args) -> Self::Output;
  4. }
  5. pub trait FnMut<Args> : FnOnce<Args> {
  6. extern "rust-call" fn call_mut(&mut self, args: Args) -> Self::Output;
  7. }
  8. pub trait FnOnce<Args> {
  9. type Output;
  10. extern "rust-call" fn call_once(self, args: Args) -> Self::Output;
  11. }
  12. # }

你会注意到这些 trait 之间的些许区别,不过一个大的区别是selfFn获取&selfFnMut获取&mut self,而FnOnce获取self。这包含了所有3种通过通常函数调用语法的self。不过我们将它们分在 3 个 trait 里,而不是单独的 1 个。这给了我们大量的对于我们可以使用哪种闭包的控制。

闭包的|| {}语法是上面 3 个 trait 的语法糖。Rust 将会为了环境创建一个结构体,impl合适的 trait,并使用它。

闭包作为参数

现在我们知道了闭包是 trait,我们已经知道了如何接受和返回闭包;就像任何其它的 trait!

这也意味着我们也可以选择静态或动态分发。首先,让我们写一个函数,它接受可调用的参数,调用之,然后返回结果:

  1. fn call_with_one<F>(some_closure: F) -> i32
  2. where F : Fn(i32) -> i32 {
  3. some_closure(1)
  4. }
  5. let answer = call_with_one(|x| x + 2);
  6. assert_eq!(3, answer);

我们传递我们的闭包,|x| x + 2,给call_with_one。它正做了我们说的:它调用了闭包,1作为参数。

让我们更深层的解析call_with_one的签名:

  1. fn call_with_one<F>(some_closure: F) -> i32
  2. where F : Fn(i32) -> i32 {
  3. some_closure(1) }

我们获取一个参数,而它有类型F。我们也返回一个i32。这一部分并不有趣。下一部分是:

  1. fn call_with_one<F>(some_closure: F) -> i32
  2. where F : Fn(i32) -> i32 {
  3. some_closure(1) }

因为Fn是一个trait,我们可以用它限制我们的泛型。在这个例子中,我们的闭包取得一个i32作为参数并返回i32,所以我们用泛型限制是Fn(i32) -> i32

还有一个关键点在于:因为我们用一个trait限制泛型,它会是单态的,并且因此,我们在闭包中使用静态分发。这是非常简单的。在很多语言中,闭包固定在堆上分配,所以总是进行动态分发。在Rust中,我们可以在栈上分配我们闭包的环境,并静态分发调用。这经常发生在迭代器和它们的适配器上,它们经常取得闭包作为参数。

当然,如果我们想要动态分发,我们也可以做到。trait对象处理这种情况,通常:

  1. fn call_with_one(some_closure: &Fn(i32) -> i32) -> i32 {
  2. some_closure(1)
  3. }
  4. let answer = call_with_one(&|x| x + 2);
  5. assert_eq!(3, answer);

现在我们取得一个trait对象,一个&Fn。并且当我们将我们的闭包传递给call_with_one时我们必须获取一个引用,所以我们使用&||

函数指针和闭包

我们已经讨论过如何将闭包传递给函数; 你也可以将常规函数传递给函数! 当您想要传递已定义的函数而不是定义新的闭包时,此技术非常有用。 使用函数指针执行此操作将允许您将函数用作其他函数的参数。 函数强制转换为fn类型。 fn类型称为函数指针。 指定参数是函数指针的语法类似于闭包的语法.

与闭包不同,fn是一种类型而不是一种 trait,因此我们直接将fn指定为参数类型,而不是将一个Fn trait声明为 trait绑定的泛型类型参数。

函数指针实现所有三个闭包 trait(Fn,FnMut和FnOnce),因此您始终可以将函数指针作为期望闭包的函数的参数传递。 最好使用泛型类型和闭包 trait之一来编写函数,这样您的函数就可以接受函数或闭包。

您希望仅接受fn而不是闭包的示例是在与没有闭包的外部代码交互时:C函数可以接受函数作为参数,但C没有闭包。

作为可以使用内联闭包或命名函数的闭包的示例,让我们看一下map的用法。 要使用map函数将数字向量转换为字符串向量,我们可以使用闭包,如下所示:

  1. // 使用闭包
  2. let list_of_numbers = vec![1, 2, 3];
  3. let list_of_strings: Vec<String> = list_of_numbers
  4. .iter()
  5. .map(|i| i.to_string())
  6. .collect();
  7. // 使用函数
  8. let list_of_numbers = vec![1, 2, 3];
  9. let list_of_strings: Vec<String> = list_of_numbers
  10. .iter()
  11. .map(ToString::to_string)
  12. .collect();

我们必须使用完全限定语法,因为有多个可用的函数名为to_string。 这里,我们使用ToString trait中定义的to_string函数,标准库已为实现Display的任何类型实现了该函数。有些人喜欢这种风格,有些人更喜欢使用封口。 他们最终编译成相同的代码,因此请使用更清晰的样式。

在这个例子中,我们并不是严格的需要这个中间变量f,函数的名字就可以了:

  1. let answer = call_with_one(&add_one);

返回闭包

闭包由traits表示,这意味着您无法直接返回闭包。 在大多数情况下,您可能希望返回trait,可以使用实现 trait的具体类型作为函数的返回值。 但是你不能用闭包这样做,因为它们没有可回收的具体类型; 例如,您不允许将函数指针fn用作返回类型。

  1. fn returns_closure() -> Fn(i32) -> i32 {
  2. |x| x + 1
  3. }

编译错误:

  1. error[E0277]: the trait bound `std::ops::Fn(i32) -> i32 + 'static:
  2. std::marker::Sized` is not satisfied
  3. -->
  4. |
  5. 1 | fn returns_closure() -> Fn(i32) -> i32 {
  6. | ^^^^^^^^^^^^^^ `std::ops::Fn(i32) -> i32 + 'static`
  7. does not have a constant size known at compile-time
  8. |
  9. = help: the trait `std::marker::Sized` is not implemented for
  10. `std::ops::Fn(i32) -> i32 + 'static`
  11. = note: the return type of a function must have a statically known size
  12. ^

该错误再次引用了Sized trait! Rust不知道存储闭包需要多少空间。 我们之前看到了解决这个问题的方法。 我们可以使用trait对象:

  1. fn returns_closure() -> Box<dyn Fn(i32) -> i32> {
  2. Box::new(|x| x + 1)
  3. }

这还有最后一个问题:

  1. error: closure may outlive the current function, but it borrows `num`,
  2. which is owned by the current function [E0373]
  3. Box::new(|x| x + num)
  4. ^~~~~~~~~~~

好吧,正如我们上面讨论的,闭包借用他们的环境。而且在这个例子中。我们的环境基于一个栈分配的5num变量绑定。所以这个借用有这个栈帧的生命周期。所以如果我们返回了这个闭包,这个函数调用将会结束,栈帧也将消失,那么我们的闭包获得了被释放的内存环境!再有最后一个修改,我们就可以让它运行了:

  1. fn factory() -> Box<Fn(i32) -> i32> {
  2. let num = 5;
  3. Box::new(move |x| x + num)
  4. }
  5. # fn main() {
  6. let f = factory();
  7. let answer = f(1);
  8. assert_eq!(6, answer);
  9. # }

通过把内部闭包变为move Fn,我们为闭包创建了一个新的栈帧。通过Box装箱,我们提供了一个已知大小的返回值,并允许它离开我们的栈帧。

使用闭包创建行为抽象

  1. use std::thread;
  2. use std::time::Duration;
  3. fn simulated_expensive_calculation(intensity: u32) -> u32 {
  4. println!("calculating slowly...");
  5. thread::sleep(Duration::from_secs(2));
  6. intensity
  7. }
  8. fn generate_workout(intensity: u32, random_number: u32) {
  9. if intensity < 25 {
  10. println!(
  11. "Today, do {} pushups!",
  12. simulated_expensive_calculation(intensity)
  13. );
  14. println!(
  15. "Next, do {} situps!",
  16. simulated_expensive_calculation(intensity)
  17. );
  18. } else {
  19. if random_number == 3 {
  20. println!("Take a break today! Remember to stay hydrated!");
  21. } else {
  22. println!(
  23. "Today, run for {} minutes!",
  24. simulated_expensive_calculation(intensity)
  25. );
  26. }
  27. }
  28. }
  29. fn main() {
  30. let simulated_user_specified_value = 10;
  31. let simulated_random_number = 7;
  32. generate_workout(
  33. simulated_user_specified_value,
  34. simulated_random_number
  35. );
  36. }

使用函数重构

  1. use std::thread;
  2. use std::time::Duration;
  3. fn simulated_expensive_calculation(intensity: u32) -> u32 {
  4. println!("calculating slowly...");
  5. thread::sleep(Duration::from_secs(2));
  6. intensity
  7. }
  8. fn generate_workout(intensity: u32, random_number: u32) {
  9. let expensive_result =
  10. simulated_expensive_calculation(intensity);
  11. if intensity < 25 {
  12. println!(
  13. "Today, do {} pushups!",
  14. expensive_result
  15. );
  16. println!(
  17. "Next, do {} situps!",
  18. expensive_result
  19. );
  20. } else {
  21. if random_number == 3 {
  22. println!("Take a break today! Remember to stay hydrated!");
  23. } else {
  24. println!(
  25. "Today, run for {} minutes!",
  26. expensive_result
  27. );
  28. }
  29. }
  30. }
  31. fn main() {
  32. let simulated_user_specified_value = 10;
  33. let simulated_random_number = 7;
  34. generate_workout(
  35. simulated_user_specified_value,
  36. simulated_random_number
  37. );
  38. }

使用闭包重构存储代码

  1. use std::thread;
  2. use std::time::Duration;
  3. let expensive_closure = |num| {
  4. println!("calculating slowly...");
  5. thread::sleep(Duration::from_secs(2));
  6. num
  7. };
  8. fn generate_workout(intensity: u32, random_number: u32) {
  9. let expensive_closure = |num| {
  10. println!("calculating slowly...");
  11. thread::sleep(Duration::from_secs(2));
  12. num
  13. };
  14. if intensity < 25 {
  15. println!(
  16. "Today, do {} pushups!",
  17. expensive_closure(intensity)
  18. );
  19. println!(
  20. "Next, do {} situps!",
  21. expensive_closure(intensity)
  22. );
  23. } else {
  24. if random_number == 3 {
  25. println!("Take a break today! Remember to stay hydrated!");
  26. } else {
  27. println!(
  28. "Today, run for {} minutes!",
  29. expensive_closure(intensity)
  30. );
  31. }
  32. }
  33. }
  34. fn main() {
  35. let simulated_user_specified_value = 10;
  36. let simulated_random_number = 7;
  37. generate_workout(
  38. simulated_user_specified_value,
  39. simulated_random_number
  40. );
  41. }

泛型参数和Fn trait存储闭包

  1. struct Cacher<T>
  2. where T: Fn(u32) -> u32
  3. {
  4. calculation: T,
  5. value: Option<u32>,
  6. }
  7. impl<T> Cacher<T>
  8. where T: Fn(u32) -> u32
  9. {
  10. fn new(calculation: T) -> Cacher<T> {
  11. Cacher {
  12. calculation,
  13. value: None,
  14. }
  15. }
  16. fn value(&mut self, arg: u32) -> u32 {
  17. match self.value {
  18. Some(v) => v,
  19. None => {
  20. let v = (self.calculation)(arg);
  21. self.value = Some(v);
  22. v
  23. },
  24. }
  25. }
  26. }
  27. fn generate_workout(intensity: u32, random_number: u32) {
  28. let mut expensive_result = Cacher::new(|num| {
  29. println!("calculating slowly...");
  30. thread::sleep(Duration::from_secs(2));
  31. num
  32. });
  33. if intensity < 25 {
  34. println!(
  35. "Today, do {} pushups!",
  36. expensive_result.value(intensity)
  37. );
  38. println!(
  39. "Next, do {} situps!",
  40. expensive_result.value(intensity)
  41. );
  42. } else {
  43. if random_number == 3 {
  44. println!("Take a break today! Remember to stay hydrated!");
  45. } else {
  46. println!(
  47. "Today, run for {} minutes!",
  48. expensive_result.value(intensity)
  49. );
  50. }
  51. }
  52. }
  53. fn main() {
  54. let simulated_user_specified_value = 10;
  55. let simulated_random_number = 7;
  56. generate_workout(
  57. simulated_user_specified_value,
  58. simulated_random_number
  59. );
  60. }