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

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

使用 Closure 捕获环境

我们首先将研究如何使用闭包来捕获 environment 中定义它们供以后使用。这是场景:每 so 通常,我们的 T 恤公司会将一件独家限量版衬衫赠送给 某人在我们的邮件列表中作为晋升。邮件列表中的人员可以 (可选)将他们最喜欢的颜色添加到他们的配置文件中。如果为 免费衬衫有他们最喜欢的颜色集,他们得到那个颜色的衬衫。如果 person 没有指定最喜欢的颜色,他们得到公司的任何颜色 目前拥有最多的。

有很多方法可以实现这一点。在此示例中,我们将使用 enum 调用ShirtColor具有RedBlue(限制 为简单起见,可用的颜色数量)。我们代表公司的 具有Inventorystruct 中,该 struct 具有一个名为shirts那 包含一个Vec<ShirtColor>代表当前库存的衬衫颜色。 方法giveaway定义日期Inventory获取可选衬衫 免费衬衫获胜者的 color 首选项,并返回 人会得到。这个设置如示例 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:衬衫公司赠品情况

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

同样,此代码可以通过多种方式实现,在这里,重点介绍 闭包,我们坚持使用您已经学过的概念,除了 这giveaway方法。在giveaway方法,我们得到 用户首选项作为 typeOption<ShirtColor>并调用unwrap_or_elsemethod 开启user_preference.这unwrap_or_elsemethod 开启Option<T>由标准库定义。 它需要一个参数:一个没有任何参数的闭包,它返回一个值T(存储在Some变体Option<T>,在本例中ShirtColor).如果Option<T>Some变体unwrap_or_else返回Some.如果Option<T>None变体unwrap_or_else调用闭包并返回 关闭。

我们指定 closure 表达式|| self.most_stocked()作为参数unwrap_or_else.这是一个本身不带参数的闭包(如果 closure 有参数,它们会出现在两个垂直条之间)。这 闭包调用的主体self.most_stocked().我们正在定义闭包 here 和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类型 we defined,或者我们想在这个场景中使用的 logic。该闭包捕获了一个 immutable 引用传递给self Inventory实例,并使用 code 中,我们指定给unwrap_or_else方法。另一方面,函数 无法以这种方式捕获其环境。

Closure 类型推理和注释

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

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

与变量一样,如果我们想增加 明确和清晰,但代价是比严格来说更冗长 必要。注释闭包的类型类似于定义 如示例 13-2 所示。在这个例子中,我们定义了一个闭包并存储它 而不是在 spot 中定义闭包,我们将其作为 参数,就像我们在示例 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 和 具有相同行为的 Closure 以进行比较。我们添加了一些空间 对齐相关部分。这说明了闭包语法的相似之处 to 函数语法,但管道的使用和 自选:

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_v3add_one_v4lines 要求将闭包评估为 能够编译,因为类型将从它们的使用情况中推断出来。这是 似let v = Vec::new();需要类型注释或 some 类型插入到Vec以便 Rust 能够推断类型。

对于闭包定义,编译器将为每个 它们的参数和它们的返回值。例如,示例 13-3 显示了 短闭包的定义,它只返回它作为 参数。这个 closure 不是很有用,除非是为了这个 例。请注意,我们没有在定义中添加任何类型注释。 因为没有类型注解,所以我们可以调用任何类型的闭包, 我们在这里已经完成了String第一次。如果我们随后尝试调用example_closure如果为 Integer,则会收到 error。

文件名: 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

我们第一次调用example_closure使用Stringvalue 时,编译器 推断x以及 Closure 的返回类型为String.那些 类型被锁定到example_closure,我们得到一个类型 当我们下次尝试使用具有相同闭包的不同类型时出错。

捕获引用或移动所有权

闭包可以通过三种方式从其环境中捕获值,即 直接映射到函数可以采用参数的三种方式:借用 不可变地借用,并取得所有权。关闭将决定 根据函数体对 捕获的值。

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

文件名: 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:定义和调用捕获不可变引用的闭包

这个例子还说明了变量可以绑定到闭包定义 我们稍后可以使用变量 name 和括号来调用 closure 如果变量名称是函数名称。

因为我们可以有多个不可变的引用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]

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

如果你想强制闭包获得它在 environment 的 shell 中,即使 body 并不严格需要 所有权,您可以使用move关键字。

当将闭包传递给要移动的新线程时,这种技术最有用 数据,使其归新线程所有。我们将讨论线程及其原因 您可能希望在第 16 章中详细使用它们,当我们讨论 并发,但现在,让我们简要地探索一下使用 需要move关键词。示例 13-6 显示了示例 13-4 已修改 要在新线程中打印向量,而不是在主线程中打印向量:

文件名: 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需要打印它。在此示例中,即使闭包主体 still 只需要一个不可变的引用,我们需要指定list应该 移动到闭包中,方法是将move关键字的开头 闭包定义。新线程可能会在主线程的其余部分之前完成 thread 完成,或者 main thread 可能会先完成。如果主线程 保持所有权list但在新线程之前结束并丢弃list,则线程中的不可变引用将无效。因此, 编译器要求list被移动到给新线程的闭包中 因此,引用将是有效的。尝试删除move关键字或使用list在主线程中定义闭包后,查看您 获取!

将捕获的值移出 Closure 和Fn性状

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

闭包从环境中捕获和处理值的方式会影响 闭包实现了哪些 trait,而 traits 是函数和结构体的方式 可以指定他们可以使用的闭包类型。闭包将自动 实现其中一项、两项或全部 3 项Fn性状,以加法方式, 取决于 Closure 的 body 如何处理这些值:

  1. FnOnce适用于可以调用一次的闭包。所有 closure 都实现 至少这个 trait,因为所有的闭包都可以被调用。一个 将捕获的值移出其主体只会实现FnOnce也没有 另一个Fntrait 的 trait 中调用,因为它只能调用一次。
  2. FnMut适用于不将捕获的值移出其 body 的 body 进行转换,但这可能会改变捕获的值。这些 Closure 可以是 多次被调用。
  3. Fn适用于不会将捕获的值移出其主体的闭包 并且不会改变捕获的值,以及捕获 没有来自他们的环境。这些闭包可以被多次调用 而无需更改其环境,这在 同时多次调用 closure。

让我们看看unwrap_or_elsemethod 开启Option<T>那 我们在示例 13-1 中使用了:

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是泛型类型,表示Some的 variantOption.那个类型T也是unwrap_or_elsefunction:调用unwrap_or_elseOption<String>,将获得一个String.

接下来,请注意,unwrap_or_elsefunction 具有额外的泛型 参数F.这Ftype 是名为f,即 我们在调用unwrap_or_else.

在泛型类型上指定的 trait boundFFnOnce() -> T哪 方法F必须能够被调用一次,不接受任何参数,并返回一个T. 用FnOnce在 trait bound 中表示 constraintunwrap_or_else我只打算打电话f最多一次。在 bodyunwrap_or_else,我们可以看到,如果OptionSome,f不会 叫。如果OptionNone,f将被调用一次。因为所有 闭包实现FnOnce,unwrap_or_else接受所有三种 闭包,并且尽可能灵活。

注意:函数可以实现所有三个Fn特征也是如此。如果我们 Wanna do 不需要从环境中捕获值,我们可以使用 函数的名称,而不是闭包,我们需要一些 实现Fn性状。例如,在Option<Vec<T>>价值 我们可以调用unwrap_or_else(Vec::new)要获取新的空向量,如果 value 为None.

现在让我们看看标准库方法sort_by_keydefined on slices, 以查看它与unwrap_or_else以及为什么sort_by_key使用FnMut而不是FnOnce对于 trait bound。闭包获得一个参数 以对正在考虑的 slice 中当前项的引用的形式, 并返回 type 为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定义为采用FnMutclosure 是它调用 多次 Closure:切片中的每个项目一次。关闭|r| r.width不会捕获、更改或从其环境中移出任何内容,因此 它满足 trait bound 要求。

相比之下,示例 13-8 展示了一个闭包的例子,它只实现了 这FnOncetrait 的 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在排序时调用 closurelist.此代码 尝试通过推送value—一个String从闭包的 环境 - 进入sort_operations向量。闭包捕获value然后移动value通过转让value自 这sort_operations向量。此 Close 可以调用一次;尝试调用 第二次不起作用,因为value将不再位于 要推送到的环境sort_operations再!因此,此 仅实现FnOnce.当我们尝试编译此代码时,我们会收到此错误 那value不能移出 Close,因为 Closure 必须 实现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 主体中移动的行value从 环境。要解决这个问题,我们需要更改 Closure 主体,使其不会 将值移出环境。计算闭包的次数 被调用,在环境中保留一个计数器并在 闭包主体是一种更直接的计算方法。关闭 在示例 13-9 中,与sort_by_key因为它只是捕获一个 mutable 对num_sort_operationscounter 的 ,因此可以称为 more than once:

文件名: 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允许

Fntrait 在定义或使用 使用闭包。在下一节中,我们将讨论迭代器。多 Iterator 方法接受闭包参数,因此请牢记这些闭包细节 随着我们继续!

本文档由官方文档翻译而来,如有差异请以官方英文文档(https://doc.rust-lang.org/)为准