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

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

使用 Closure 捕获环境

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

有很多方法可以实现这一点。在此示例中,我们将使用 enum 调用的具有变体和 (限制 为简单起见,可用的颜色数量)。我们代表公司的 inventory 替换为一个结构体,该结构体具有一个名为 包含 表示当前库存衬衫颜色的 a。 定义的方法 on 获取可选的衬衫 免费衬衫获胜者的 color 首选项,并返回 人会得到。这个设置如示例 13-1 所示:ShirtColorRedBlueInventoryshirtsVec<ShirtColor>giveawayInventory

文件名: 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:衬衫公司赠品情况

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

同样,此代码可以通过多种方式实现,在这里,重点介绍 闭包,我们坚持使用您已经学过的概念,除了 使用闭包的方法。在该方法中,我们得到 user 首选项作为 type 的参数,并在 上调用该方法。Option<T>unwrap_or_else 方法由标准库定义。 它需要一个参数:一个没有任何参数的闭包,它返回一个值(在本例中为 , 存储在 variant 中的相同类型)。如果 是变体,则返回 中的值。如果 是变体,则调用闭包并返回由 关闭。giveawaygiveawayOption<ShirtColor>unwrap_or_elseuser_preferenceTSomeOption<T>ShirtColorOption<T>Someunwrap_or_elseSomeOption<T>Noneunwrap_or_else

我们将闭包表达式指定为 的参数。这是一个本身不带参数的闭包(如果 closure 有参数,它们会出现在两个垂直条之间)。这 body 的 .我们正在定义闭包 here 和 will 的实现评估 如果需要结果,请稍后使用。|| self.most_stocked()unwrap_or_elseself.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

这里一个有趣的方面是,我们传递了一个调用当前实例的闭包。标准库 不需要了解我们 OR 类型的任何信息 defined,或者我们想在这个场景中使用的 logic。该闭包捕获了一个 immutable 引用,并将其与 code 中。另一方面,函数 无法以这种方式捕获其环境。self.most_stocked()InventoryInventoryShirtColorselfInventoryunwrap_or_else

Closure 类型推理和注释

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

闭包通常很短,并且仅在狭窄的上下文中相关,而不是 比在任何任意情况下都要多。在这些有限的上下文中,编译器可以 推断参数的类型和返回类型,类似于它的功能 来推断大多数变量的类型(在极少数情况下,编译器 也需要 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 只有一个表达式。这些都是 有效的定义,这些定义在调用时将产生相同的行为。和 行要求评估的闭包 能够编译,因为类型将从它们的使用情况中推断出来。这是 类似于需要类型注释或 一些类型插入到 Rust 中,以便能够推断类型。add_one_v3add_one_v4let v = Vec::new();Vec

对于闭包定义,编译器将为每个 它们的参数和它们的返回值。例如,示例 13-3 显示了 短闭包的定义,它只返回它作为 参数。这个 closure 不是很有用,除非是为了这个 例。请注意,我们没有在定义中添加任何类型注释。 因为没有类型注解,所以我们可以调用任何类型的闭包, 我们第一次在这里这样做。如果我们随后尝试使用整数进行调用,我们将得到一个错误。Stringexample_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

我们第一次使用值 Compiler 调用 将 的类型和 的返回类型推断为 。那些 然后,类型被锁定到 中的 Closure 中,我们得到一个类型 当我们下次尝试使用具有相同闭包的不同类型时出错。example_closureStringxStringexample_closure

捕获引用或移动所有权

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

在示例 13-4 中,我们定义了一个闭包,它捕获了对 命名的 vector 是因为它只需要一个不可变的引用来打印 值: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 如果变量名称是函数名称。

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

$ 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]

请注意,定义和调用 Closure: when 定义时,它会捕获一个 对 .在 Closure 之后,我们不会再次使用 Closure ,因此 mutable borrow 结束。在 closure 定义和 closure 调用时,不允许使用不可变的 borrow to print,因为没有其他 当存在可变 borrow 时,允许 borrows。尝试在那里添加一个,看看你得到什么错误信息!println!borrows_mutablyborrows_mutablylistprintln!

如果你想强制闭包获得它在 environment 的 shell 中,即使 body 并不严格需要 ownership,则可以在参数列表之前使用关键字。move

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

文件名: 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:用于强制线程获得movelist

我们生成一个新线程,给线程一个闭包作为参数运行。这 Closure body 打印出列表。在示例 13-4 中,闭包仅使用不可变引用捕获,因为这是最少的访问量 到需要打印它。在此示例中,即使闭包主体 still 只需要一个不可变的引用,我们需要指定 should 通过将关键字放在 闭包定义。新线程可能会在主线程的其余部分之前完成 thread 完成,或者 main thread 可能会先完成。如果主线程 保持所有权,但在新线程之前结束并丢弃,线程中的不可变引用将无效。因此, compiler 需要将其移动到给定给新线程的闭包 因此,引用将是有效的。在定义闭包后,尝试在主线程中删除关键字或使用,以查看您 获取!listlistlistmovelistlistlistmovelist

将捕获的值移出 Closure 和 fn trait

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

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

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

让我们看看该方法的定义 我们在示例 13-1 中使用了:unwrap_or_elseOption<T>

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(),
        }
    }
}

回想一下,它是泛型类型,表示 .该类型也是函数的返回类型:例如,调用 , 的代码将获得 .TSomeOptionTunwrap_or_elseunwrap_or_elseOption<String>String

接下来,请注意该函数具有额外的泛型 参数。type 是名为 的参数的类型,即 我们在调用 .unwrap_or_elseFFfunwrap_or_else

在泛型类型上指定的 trait bound 是 ,其中 means 必须能够被调用一次,不接受任何参数,并返回一个 . 在 trait bound 中使用表示最多只调用一次的约束。在 的主体中,我们可以看到,如果 是 ,则不会是 叫。如果是 ,则将被调用一次。因为所有 闭包实现 ,接受所有三种 闭包,并且尽可能灵活。FFnOnce() -> TFTFnOnceunwrap_or_elsefunwrap_or_elseOptionSomefOptionNonefFnOnceunwrap_or_else

注意: 函数也可以实现所有三个 trait。如果我们 Wanna do 不需要从环境中捕获值,我们可以使用 函数的名称,而不是闭包,我们需要一些 实现其中一个特征。例如,在值 如果 值为 。FnFnOption<Vec<T>>unwrap_or_else(Vec::new)None

现在让我们看看在 slice 上定义的标准库方法, 以查看它与 bound 的 trait 有何不同,以及为什么使用 而不是 for trait bound。闭包获得一个参数 以对正在考虑的 slice 中当前项的引用的形式, 并返回 type 为 can be ordered 的值。此功能非常有用 当您想按每个项目的特定属性对切片进行排序时。在 示例 13-7,我们有一个实例列表,我们用它来按照它们的属性从低到高排序:sort_by_keyunwrap_or_elsesort_by_keyFnMutFnOnceKRectanglesort_by_keywidth

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

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

相比之下,示例 13-8 展示了一个闭包的例子,它只实现了 trait 的 trait 中,因为它将一个值移出环境。这 编译器不允许我们将这个闭包与 :FnOncesort_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:尝试使用FnOncesort_by_key

这是一种人为的、复杂的方法(不起作用)来尝试计算 排序时调用 Closure 的次数。此代码 尝试通过从 Closure 的 environment - 到向量中。然后,Closure 捕获通过将 的所有权转移到 向量。此 Close 可以调用一次;尝试调用 它第二次不起作用,因为不再位于 环境再次被推入!因此,此 仅实现 .当我们尝试编译此代码时,我们会收到此错误 不能从 Closure 中移出,因为 Closure 必须 实现:sort_by_keylistvalueStringsort_operationsvaluevaluevaluesort_operationsvaluesort_operationsFnOncevalueFnMut

$ 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 主体,使其不会 将值移出环境。计算闭包的次数 被调用,在环境中保留一个计数器并在 闭包主体是一种更直接的计算方法。关闭 在示例 13-9 中有效,因为它只捕获一个 mutable 引用 counter ,因此可以称为 more than once:valuesort_by_keynum_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:允许使用 closure withFnMutsort_by_key

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

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