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


Rust 的闭包是匿名函数,你可以保存在变量中或作为参数传递给其他函数。你可以在一个地方创建闭包,然后在其他位置调用闭包,以便在不同的上下文中评估它。与函数不同,闭包可以从定义它们的范围内捕获值。我们将演示这些 closure 功能如何允许代码重用和行为自定义。


使用 Closure 捕获环境


我们首先将研究如何使用闭包从定义它们的环境中捕获值以供以后使用。情况是这样的:每隔一段时间,我们的 T 恤公司就会向我们邮件列表中的某人赠送一件独家限量版衬衫作为促销活动。邮件列表中的人员可以选择将他们最喜欢的颜色添加到他们的个人资料中。如果被选中获得免费衬衫的人有他们最喜欢的颜色集,他们就会得到该颜色的衬衫。如果该用户没有指定最喜欢的颜色,他们将获得公司当前最常用的任何颜色。


有很多方法可以实现这一点。在此示例中,我们将使用一个名为 ShirtColor 的枚举,该枚举具有变体 RedBlue(为简单起见,限制了可用的颜色数量)。我们使用一个 Inventory 结构来表示公司的库存,该结构具有一个名为 shirts 的字段,其中包含一个 Vec<ShirtColor>,表示当前库存中的衬衫颜色。在 Inventory 上定义的方法 giveaway 获取免费衬衫获胜者的可选衬衫颜色首选项,并返回该人员将获得的衬衫颜色。这个设置如示例 13-1 所示:


文件名: src/main.rs
#[derive(Debug, PartialEq, Copy, Clone)]
enum ShirtColor {
    Red,
    Blue,
}

struct Inventory {
    shirts: Vec<ShirtColor>,
}

impl Inventory {
    fn giveaway(&self, user_preference: Option<ShirtColor>) -> ShirtColor {
        user_preference.unwrap_or_else(|| self.most_stocked())
    }

    fn most_stocked(&self) -> ShirtColor {
        let mut num_red = 0;
        let mut num_blue = 0;

        for color in &self.shirts {
            match color {
                ShirtColor::Red => num_red += 1,
                ShirtColor::Blue => num_blue += 1,
            }
        }
        if num_red > num_blue {
            ShirtColor::Red
        } else {
            ShirtColor::Blue
        }
    }
}

fn main() {
    let store = Inventory {
        shirts: vec![ShirtColor::Blue, ShirtColor::Red, ShirtColor::Blue],
    };

    let user_pref1 = Some(ShirtColor::Red);
    let giveaway1 = store.giveaway(user_pref1);
    println!(
        "The user with preference {:?} gets {:?}",
        user_pref1, giveaway1
    );

    let user_pref2 = None;
    let giveaway2 = store.giveaway(user_pref2);
    println!(
        "The user with preference {:?} gets {:?}",
        user_pref2, giveaway2
    );
}

示例 13-1:衬衫公司赠品情况


main 中定义的商店还剩下两件蓝色衬衫和一件红色衬衫来分发此限量版促销活动。我们为偏好红色衬衫的用户和没有任何偏好的用户调用 giveaway 方法。


同样,这段代码可以通过多种方式实现,在这里,为了专注于闭包,我们坚持使用您已经学过的概念,除了使用 closure 的 giveaway 方法的主体。在 giveaway 方法中,我们获取用户首选项作为 Option<ShirtColor> 类型的参数,并调用 unwrap_or_else user_preference 方法。unwrap_or_else 方法 选件<T>由标准库定义。它需要一个参数:一个不带任何参数的闭包,返回一个值 T (在本例中,存储在 Option<T>Some 变体中的相同类型 ShirtColor 的 ShirtColor )。如果 Option<T>Some 变体,则unwrap_or_else 返回 Some 中的值。如果选项<T>None variant unwrap_or_else调用闭包并返回闭包返回的值。


我们指定闭包表达式 || self.most_stocked() 作为 unwrap_or_else。这是一个本身不带参数的闭包(如果闭包有参数,它们将出现在两个垂直条之间)。闭包的主体调用 self.most_stocked()。我们在这里定义了闭包,如果需要结果,unwrap_or_else 的实现将在稍后评估闭包。


运行此代码将打印:

$ cargo run
   Compiling shirt-company v0.1.0 (file:///projects/shirt-company)
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.27s
     Running `target/debug/shirt-company`
The user with preference Some(Red) gets Red
The user with preference None gets Blue


一个有趣的方面是,我们传递了一个闭包,该闭包调用 self.most_stocked() 在当前 Inventory 实例上。标准库不需要了解我们定义的 InventoryShirtColor 类型的任何信息,也不需要了解我们想要在此场景中使用的逻辑。闭包捕获对 selfInventory 实例的不可变引用,并将其与我们指定的代码一起传递给 unwrap_or_else 方法。另一方面,函数无法以这种方式捕获其环境。


Closure 类型推理和注释


函数和闭包之间有更多的区别。闭包通常不需要你像 fn 函数那样注释参数的类型或返回值。函数需要类型注释,因为这些类型是向用户公开的显式接口的一部分。严格定义此接口对于确保每个人都就函数使用和返回的值类型达成一致非常重要。另一方面,闭包不会在像这样的公开接口中使用:它们存储在变量中,无需命名和公开给我们库的用户即可使用。


闭包通常很简短,并且仅在狭窄的上下文中相关,而不是在任何任意的场景中。在这些有限的上下文中,编译器可以推断参数类型和返回类型,类似于它能够推断大多数变量的类型(在极少数情况下,编译器也需要闭包类型注释)。


与变量一样,如果我们想增加显式性和清晰度,我们可以添加类型注释,但代价是比严格必要的更详细。为闭包的类型添加注解类似于示例 13-2 中所示的定义。在这个例子中,我们定义了一个闭包并将其存储在一个变量中,而不是像示例 13-1 中那样,在将闭包作为参数传递的位置定义闭包。


文件名: src/main.rs
use std::thread;
use std::time::Duration;

fn generate_workout(intensity: u32, random_number: u32) {
    let expensive_closure = |num: u32| -> u32 {
        println!("calculating slowly...");
        thread::sleep(Duration::from_secs(2));
        num
    };

    if intensity < 25 {
        println!("Today, do {} pushups!", expensive_closure(intensity));
        println!("Next, do {} situps!", expensive_closure(intensity));
    } else {
        if random_number == 3 {
            println!("Take a break today! Remember to stay hydrated!");
        } else {
            println!(
                "Today, run for {} minutes!",
                expensive_closure(intensity)
            );
        }
    }
}

fn main() {
    let simulated_user_specified_value = 10;
    let simulated_random_number = 7;

    generate_workout(simulated_user_specified_value, simulated_random_number);
}

示例 13-2:在闭包中添加参数和返回值类型的可选类型注释


添加类型注释后,闭包的语法看起来更类似于函数的语法。在这里,我们定义了一个将 1 添加到其参数的函数和一个具有相同行为的闭包,以进行比较。我们添加了一些空格来排列相关部分。这说明了闭包语法与函数语法的相似之处,除了管道的使用和可选的语法数量:

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  ;


第一行显示函数定义,第二行显示完全 带注释的闭包定义。在第三行中,我们删除了类型注释 从 Closure 定义。在第四行中,我们去掉括号,即 是可选的,因为 Closed Body 只有一个表达式。这些都是 有效的定义,这些定义在调用时将产生相同的行为。这 add_one_v3 行和 add_one_v4 行要求对闭包进行评估才能编译,因为类型将从它们的用法中推断出来。这类似于 let v = Vec::new();需要将类型注释或某种类型的值插入到 Vec 中,以便 Rust 能够推断类型。


对于闭包定义,编译器将为其每个参数及其返回值推断一个具体类型。例如,示例 13-3 显示了一个短闭包的定义,它只返回它作为参数接收的值。除了此示例的目的外,此 closure 不是很有用。请注意,我们没有在定义中添加任何类型注释。因为没有类型注解,所以我们可以调用任何类型的闭包,我们在这里第一次使用 String 就这样做了。如果我们随后尝试调用 example_closure 整数,我们将收到错误。


文件名: src/main.rs
fn main() {
    let example_closure = |x| x;

    let s = example_closure(String::from("hello"));
    let n = example_closure(5);
}

示例 13-3:尝试调用一个类型由两种不同类型的


编译器给我们这个错误:

$ cargo run
   Compiling closure-example v0.1.0 (file:///projects/closure-example)
error[E0308]: mismatched types
 --> src/main.rs:5:29
  |
5 |     let n = example_closure(5);
  |             --------------- ^- help: try using a conversion method: `.to_string()`
  |             |               |
  |             |               expected `String`, found integer
  |             arguments to this function are incorrect
  |
note: expected because the closure was earlier called with an argument of type `String`
 --> src/main.rs:4:29
  |
4 |     let s = example_closure(String::from("hello"));
  |             --------------- ^^^^^^^^^^^^^^^^^^^^^ expected because this argument is of type `String`
  |             |
  |             in this closure call
note: closure parameter defined here
 --> src/main.rs:2:28
  |
2 |     let example_closure = |x| x;
  |                            ^

For more information about this error, try `rustc --explain E0308`.
error: could not compile `closure-example` (bin "closure-example") due to 1 previous error


当我们第一次使用 String 值调用 example_closure 时,编译器会推断 x 的类型和闭包的返回类型为 String。然后这些类型被锁定到 example_closure 的闭包中,当我们下次尝试使用具有相同闭包的不同类型时,我们会得到一个类型错误。


捕获引用或移动所有权


闭包可以通过三种方式从其环境中捕获值,这直接映射到函数可以采用参数的三种方式:不可变借用、可变借用和获取所有权。闭包将根据函数体对捕获的值执行的作来决定使用其中的哪些。


在示例 13-4 中,我们定义了一个闭包,它捕获了对名为 list 的 vector 的不可变引用,因为它只需要一个不可变的引用来打印值:


文件名: src/main.rs
fn main() {
    let list = vec![1, 2, 3];
    println!("Before defining closure: {list:?}");

    let only_borrows = || println!("From closure: {list:?}");

    println!("Before calling closure: {list:?}");
    only_borrows();
    println!("After calling closure: {list:?}");
}

示例 13-4:定义和调用捕获不可变引用的闭包


这个例子还说明了变量可以绑定到闭包定义,我们稍后可以使用变量名和括号来调用闭包,就好像变量名是函数名一样。


因为我们可以同时有多个不可变的 list 引用, list 仍然可以从闭包定义之前、闭包定义之后、调用闭包之前以及调用闭包之后的代码中访问。此代码编译、运行和打印:

$ cargo run
     Locking 1 package to latest compatible version
      Adding closure-example v0.1.0 (/Users/chris/dev/rust-lang/book/tmp/listings/ch13-functional-features/listing-13-04)
   Compiling closure-example v0.1.0 (file:///projects/closure-example)
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.43s
     Running `target/debug/closure-example`
Before defining closure: [1, 2, 3]
Before calling closure: [1, 2, 3]
From closure: [1, 2, 3]
After calling closure: [1, 2, 3]


接下来,在示例 13-5 中,我们更改了闭包主体,使其向 list 向量添加了一个元素。闭包现在捕获一个可变引用:


文件名: src/main.rs
fn main() {
    let mut list = vec![1, 2, 3];
    println!("Before defining closure: {list:?}");

    let mut borrows_mutably = || list.push(7);

    borrows_mutably();
    println!("After calling closure: {list:?}");
}

示例 13-5:定义和调用捕获可变引用的闭包


此代码编译、运行和打印:

$ cargo run
     Locking 1 package to latest compatible version
      Adding closure-example v0.1.0 (/Users/chris/dev/rust-lang/book/tmp/listings/ch13-functional-features/listing-13-05)
   Compiling closure-example v0.1.0 (file:///projects/closure-example)
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.43s
     Running `target/debug/closure-example`
Before defining closure: [1, 2, 3]
After calling closure: [1, 2, 3, 7]


请注意,在定义和borrows_mutably闭包的调用之间不再有 println!:定义 borrows_mutably 时,它会捕获对 list 的可变引用。在调用 closure 后,我们不会再次使用 Closure,因此 mutable borrow 结束。在闭包定义和闭包调用之间,不允许使用 immutable borrow to print,因为当存在可变借款时不允许其他借用。尝试添加 println! 在那里查看您收到什么错误消息!


如果你想强制闭包获得它在环境中使用的值的所有权,即使闭包的主体并不严格需要所有权,你可以在参数列表之前使用 move 关键字。


当将闭包传递给新线程以移动数据以使其归新线程所有时,这种技术最有用。我们将在第 16 章中讨论并发性时详细讨论线程以及为什么要使用它们,但现在,让我们简要地探索一下使用需要 move 关键字的闭包生成新线程。示例 13-6 显示了示例 13-4 被修改为在新线程而不是主线程中打印 vector:


文件名: src/main.rs
use std::thread;

fn main() {
    let list = vec![1, 2, 3];
    println!("Before defining closure: {list:?}");

    thread::spawn(move || println!("From thread: {list:?}"))
        .join()
        .unwrap();
}

示例 13-6:使用 move 强制线程的闭包获得 list 的所有权


我们生成一个新线程,给线程一个闭包作为参数运行。这 Closure body 打印出列表。在示例 13-4 中,闭包只捕获了 list 使用不可变引用,因为这是打印它所需的最少 list 访问权限。在这个例子中,即使闭包体仍然只需要一个不可变的引用,我们也需要通过将 move 关键字放在闭包定义的开头来指定该列表应该被移动到闭包中。新线程可能在主线程的其余部分完成之前完成,或者主线程可能先完成。如果主线程保留了 list 的所有权,但在新线程之前结束并丢弃了 list 中,线程中的不可变引用将无效。因此,编译器要求将 list 移动到给定给新线程的闭包中,以便引用有效。尝试删除 move 关键字或使用 list 在主线程中定义闭包后,查看您 获取!


将捕获的值移出 Closure 和 fn trait


一旦闭包从定义闭包的环境中捕获了值的引用或捕获了所有权(从而影响了移动到闭包中的内容(如果有的话),闭包主体中的代码定义了稍后评估闭包时引用或值会发生什么(从而影响了什么, 如果有的话,从 closure 中移出)。闭包主体可以执行以下任何作:将捕获的值移出闭包,更改捕获的值,既不移动也不更改值,或者从一开始就不从环境中捕获任何内容。


闭包从环境中捕获和处理值的方式会影响闭包实现的 trait,而 trait 是函数和结构体指定它们可以使用的闭包类型的方式。闭包将以加法方式自动实现这些 Fn 特征中的一个、两个或全部三个,具体取决于闭包的主体如何处理这些值:


  1. FnOnce 适用于可以调用一次的闭包。所有闭包都至少实现了这个 trait,因为所有的闭包都可以被调用。将捕获的值移出其主体的闭包将仅实现 FnOnce,而不会实现其他 Fn trait,因为它只能调用一次。

  2. FnMut 适用于不会将捕获的值移出其主体的闭包,但这可能会改变捕获的值。这些闭包可以多次调用。

  3. Fn 适用于不将捕获的值移出其主体且不改变捕获值的闭包,以及不从其环境中捕获任何内容的闭包。这些闭包可以被多次调用而不会改变其环境,这在并发多次调用闭包等情况下很重要。


让我们看看示例 13-1 中使用的 Option<T> 上的 unwrap_or_else 方法的定义:

impl<T> Option<T> {
    pub fn unwrap_or_else<F>(self, f: F) -> T
    where
        F: FnOnce() -> T
    {
        match self {
            Some(x) => x,
            None => f(),
        }
    }
}


回想一下,T 是泛型类型,表示 Option某种变体。该类型 T 也是 unwrap_or_else 函数unwrap_or_else:在 例如,Option<String> 将得到一个 String


接下来,请注意 unwrap_or_else 函数具有额外的泛型类型参数 FF 类型是名为 f 的参数的类型,这是我们在调用 unwrap_or_else 时提供的闭包。


在泛型类型 F 上指定的 trait 边界是 FnOnce() -> T,这意味着 F 必须能够被调用一次,不接受任何参数,并返回一个 T。在 trait bound 中使用 FnOnce 表示 constraint unwrap_or_else 最多只调用 f 一次。在 body unwrap_or_else,我们可以看到,如果 OptionSome,则不会调用 f。如果 OptionNone则 f 将被调用一次。因为所有的闭包都实现了 FnOnce,所以 unwrap_or_else 接受所有三种类型的闭包,并且尽可能灵活。


注意:函数也可以实现所有三个 Fn 特征。如果我们想要做的事情不需要从环境中捕获值,那么在我们需要实现 Fn trait 之一的地方,我们可以使用函数的名称而不是闭包。例如,在 Option<Vec<T>> 值上,如果值为 None,我们可以调用 unwrap_or_else(Vec::new) 来获取一个新的空向量。


现在让我们看看sort_by_key slices 上定义的标准库方法,看看它与 unwrap_or_else 有何不同,以及为什么 sort_by_key 使用 FnMut 而不是 FnOnce 作为 trait bound 的 FnOnce 进行设置。闭包以引用正在考虑的 slice 中当前项目的形式获取一个参数,并返回一个可以排序的 K 类型的值。当您想按每个项目的特定属性对切片进行排序时,此函数非常有用。在示例 13-7 中,我们有一个 Rectangle 实例列表,我们使用 sort_by_key 要按它们的 width 属性从低到高对它们进行排序:


文件名: src/main.rs
#[derive(Debug)]
struct Rectangle {
    width: u32,
    height: u32,
}

fn main() {
    let mut list = [
        Rectangle { width: 10, height: 1 },
        Rectangle { width: 3, height: 5 },
        Rectangle { width: 7, height: 12 },
    ];

    list.sort_by_key(|r| r.width);
    println!("{list:#?}");
}

示例 13-7:使用 sort_by_key 按宽度对矩形进行排序


此代码打印:

$ cargo run
   Compiling rectangles v0.1.0 (file:///projects/rectangles)
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.41s
     Running `target/debug/rectangles`
[
    Rectangle {
        width: 3,
        height: 5,
    },
    Rectangle {
        width: 7,
        height: 12,
    },
    Rectangle {
        width: 10,
        height: 1,
    },
]


sort_by_key被定义为采用 FnMut 闭包的原因是它多次调用闭包:切片中的每个项目一次。闭包 |r| r.width 不会从其环境中捕获、改变或移出任何内容,因此它满足 trait 绑定要求。


相比之下,示例 13-8 展示了一个只实现 FnOnce trait 的闭包示例,因为它将一个值移出环境。编译器不允许我们将这个闭包与 sort_by_key一起使用:


文件名: src/main.rs
#[derive(Debug)]
struct Rectangle {
    width: u32,
    height: u32,
}

fn main() {
    let mut list = [
        Rectangle { width: 10, height: 1 },
        Rectangle { width: 3, height: 5 },
        Rectangle { width: 7, height: 12 },
    ];

    let mut sort_operations = vec![];
    let value = String::from("closure called");

    list.sort_by_key(|r| {
        sort_operations.push(value);
        r.width
    });
    println!("{list:#?}");
}

示例 13-8:尝试将 FnOnce 闭包与 sort_by_key 一起使用


这是一种人为的、复杂的方法(不起作用),试图计算 sort_by_key 在排序列表时调用闭包的次数。此代码尝试通过将 value(来自闭包环境的 String)推送到 sort_operations 向量来执行此计数。闭包捕获价值 然后通过将 value 的所有权转移到 sort_operations 向量来将 value 移出 closure。此 Close 可以调用一次;尝试第二次调用它不会奏效,因为 value 将不再存在于环境中,可以再次推入 sort_operations!因此,此 closure 仅实现 FnOnce。当我们尝试编译这段代码时,我们得到这个错误,即 value 不能从闭包中移出,因为闭包必须实现 FnMut

$ cargo run
   Compiling rectangles v0.1.0 (file:///projects/rectangles)
error[E0507]: cannot move out of `value`, a captured variable in an `FnMut` closure
  --> src/main.rs:18:30
   |
15 |     let value = String::from("closure called");
   |         ----- captured outer variable
16 |
17 |     list.sort_by_key(|r| {
   |                      --- captured by this `FnMut` closure
18 |         sort_operations.push(value);
   |                              ^^^^^ move occurs because `value` has type `String`, which does not implement the `Copy` trait
   |
help: consider cloning the value if the performance cost is acceptable
   |
18 |         sort_operations.push(value.clone());
   |                                   ++++++++

For more information about this error, try `rustc --explain E0507`.
error: could not compile `rectangles` (bin "rectangles") due to 1 previous error


该错误指向 Closure body 中将 value 移出环境的行。要解决这个问题,我们需要更改 Closure 主体,使其不会将值移出环境。要计算闭包被调用的次数,在环境中保留一个计数器并在闭包主体中增加其值是一种更直接的计算方法。示例 13-9 中的闭包与 sort_by_key 一起工作,因为它只捕获对 num_sort_operations 计数器的可变引用,因此可以多次调用:


文件名: src/main.rs
#[derive(Debug)]
struct Rectangle {
    width: u32,
    height: u32,
}

fn main() {
    let mut list = [
        Rectangle { width: 10, height: 1 },
        Rectangle { width: 3, height: 5 },
        Rectangle { width: 7, height: 12 },
    ];

    let mut num_sort_operations = 0;
    list.sort_by_key(|r| {
        num_sort_operations += 1;
        r.width
    });
    println!("{list:#?}, sorted in {num_sort_operations} operations");
}

示例 13-9:允许将 FnMut 闭包与 sort_by_key 一起使用


Fn trait 在定义或使用使用闭包的函数或类型时非常重要。在下一节中,我们将讨论迭代器。许多迭代器方法都接受闭包参数,因此在我们继续时请牢记这些闭包细节!