闭包:捕获其环境的匿名函数
Rust 的闭包是匿名函数,你可以保存在变量中或作为 参数分配给其他函数。您可以在一个位置创建闭包,然后 在其他位置调用 Closure 以在不同的上下文中评估它。与 函数,闭包可以从定义它们的范围内捕获值。 我们将演示这些 Closure 功能如何允许代码重用和行为 定制。
使用 Closure 捕获环境
我们首先将研究如何使用闭包来捕获 environment 中定义它们供以后使用。这是场景:每 so 通常,我们的 T 恤公司会将一件独家限量版衬衫赠送给 某人在我们的邮件列表中作为晋升。邮件列表中的人员可以 (可选)将他们最喜欢的颜色添加到他们的配置文件中。如果为 免费衬衫有他们最喜欢的颜色集,他们得到那个颜色的衬衫。如果 person 没有指定最喜欢的颜色,他们得到公司的任何颜色 目前拥有最多的。
有很多方法可以实现这一点。在此示例中,我们将使用
enum 调用ShirtColor
具有Red
和Blue
(限制
为简单起见,可用的颜色数量)。我们代表公司的
具有Inventory
struct 中,该 struct 具有一个名为shirts
那
包含一个Vec<ShirtColor>
代表当前库存的衬衫颜色。
方法giveaway
定义日期Inventory
获取可选衬衫
免费衬衫获胜者的 color 首选项,并返回
人会得到。这个设置如示例 13-1 所示:
#[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
);
}
这store
定义于main
还剩下两件蓝衫和一件红衫
分发此限量版促销活动。我们调用giveaway
方法
适用于偏爱红色衬衫的用户和没有任何偏爱的用户。
同样,此代码可以通过多种方式实现,在这里,重点介绍
闭包,我们坚持使用您已经学过的概念,除了
这giveaway
方法。在giveaway
方法,我们得到
用户首选项作为 typeOption<ShirtColor>
并调用unwrap_or_else
method 开启user_preference
.这unwrap_or_else
method 开启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
实例。标准库
不需要了解任何有关Inventory
或ShirtColor
类型 we
defined,或者我们想在这个场景中使用的 logic。该闭包捕获了一个
immutable 引用传递给self
Inventory
实例,并使用
code 中,我们指定给unwrap_or_else
方法。另一方面,函数
无法以这种方式捕获其环境。
Closure 类型推理和注释
函数和闭包之间有更多的区别。闭包不会
通常需要你对参数的类型或返回值进行注释
喜欢fn
函数可以。函数需要类型注释,因为
类型是向用户公开的显式接口的一部分。定义
Interface Rigidly 对于确保每个人都就哪些类型达成一致非常重要
函数使用并返回的值。另一方面,不使用闭包
在像这样的公开接口中:它们存储在变量中,并在没有
命名它们并将它们公开给我们库的用户。
闭包通常很短,并且仅在狭窄的上下文中相关,而不是 比在任何任意情况下都要多。在这些有限的上下文中,编译器可以 推断参数的类型和返回类型,类似于它的功能 来推断大多数变量的类型(在极少数情况下,编译器 也需要 Closure 类型注释)。
与变量一样,如果我们想增加 明确和清晰,但代价是比严格来说更冗长 必要。注释闭包的类型类似于定义 如示例 13-2 所示。在这个例子中,我们定义了一个闭包并存储它 而不是在 spot 中定义闭包,我们将其作为 参数,就像我们在示例 13-1 中所做的那样。
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); }
添加类型注释后,闭包的语法看起来更类似于 函数语法。在这里,我们定义了一个函数,该函数的参数加 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_v3
和add_one_v4
lines 要求将闭包评估为
能够编译,因为类型将从它们的使用情况中推断出来。这是
似let v = Vec::new();
需要类型注释或
some 类型插入到Vec
以便 Rust 能够推断类型。
对于闭包定义,编译器将为每个
它们的参数和它们的返回值。例如,示例 13-3 显示了
短闭包的定义,它只返回它作为
参数。这个 closure 不是很有用,除非是为了这个
例。请注意,我们没有在定义中添加任何类型注释。
因为没有类型注解,所以我们可以调用任何类型的闭包,
我们在这里已经完成了String
第一次。如果我们随后尝试调用example_closure
如果为 Integer,则会收到 error。
fn main() {
let example_closure = |x| x;
let s = example_closure(String::from("hello"));
let n = example_closure(5);
}
编译器给我们这个错误:
$ 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
使用String
value 时,编译器
推断x
以及 Closure 的返回类型为String
.那些
类型被锁定到example_closure
,我们得到一个类型
当我们下次尝试使用具有相同闭包的不同类型时出错。
捕获引用或移动所有权
闭包可以通过三种方式从其环境中捕获值,即 直接映射到函数可以采用参数的三种方式:借用 不可变地借用,并取得所有权。关闭将决定 根据函数体对 捕获的值。
在示例 13-4 中,我们定义了一个闭包,它捕获了对
名为list
因为它只需要一个不可变的引用来打印
值:
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:?}"); }
这个例子还说明了变量可以绑定到闭包定义 我们稍后可以使用变量 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
向量。闭包现在捕获一个可变引用:
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:?}"); }
此代码编译、运行和打印:
$ 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_mutably
closure:当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 已修改
要在新线程中打印向量,而不是在主线程中打印向量:
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(); }
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 如何处理这些值:
FnOnce
适用于可以调用一次的闭包。所有 closure 都实现 至少这个 trait,因为所有的闭包都可以被调用。一个 将捕获的值移出其主体只会实现FnOnce
也没有 另一个Fn
trait 的 trait 中调用,因为它只能调用一次。FnMut
适用于不将捕获的值移出其 body 的 body 进行转换,但这可能会改变捕获的值。这些 Closure 可以是 多次被调用。Fn
适用于不会将捕获的值移出其主体的闭包 并且不会改变捕获的值,以及捕获 没有来自他们的环境。这些闭包可以被多次调用 而无需更改其环境,这在 同时多次调用 closure。
让我们看看unwrap_or_else
method 开启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_else
function:调用unwrap_or_else
在Option<String>
,将获得一个String
.
接下来,请注意,unwrap_or_else
function 具有额外的泛型
参数F
.这F
type 是名为f
,即
我们在调用unwrap_or_else
.
在泛型类型上指定的 trait boundF
是FnOnce() -> T
哪
方法F
必须能够被调用一次,不接受任何参数,并返回一个T
.
用FnOnce
在 trait bound 中表示 constraintunwrap_or_else
我只打算打电话f
最多一次。在 bodyunwrap_or_else
,我们可以看到,如果Option
是Some
,f
不会
叫。如果Option
是None
,f
将被调用一次。因为所有
闭包实现FnOnce
,unwrap_or_else
接受所有三种
闭包,并且尽可能灵活。
注意:函数可以实现所有三个Fn
特征也是如此。如果我们
Wanna do 不需要从环境中捕获值,我们可以使用
函数的名称,而不是闭包,我们需要一些
实现Fn
性状。例如,在Option<Vec<T>>
价值
我们可以调用unwrap_or_else(Vec::new)
要获取新的空向量,如果
value 为None
.
现在让我们看看标准库方法sort_by_key
defined on slices,
以查看它与unwrap_or_else
以及为什么sort_by_key
使用FnMut
而不是FnOnce
对于 trait bound。闭包获得一个参数
以对正在考虑的 slice 中当前项的引用的形式,
并返回 type 为K
可以订购。此功能非常有用
当您想按每个项目的特定属性对切片进行排序时。在
示例 13-7,我们有一个Rectangle
实例,我们使用sort_by_key
按其width
属性从低到高:
#[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:#?}"); }
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
closure 是它调用
多次 Closure:切片中的每个项目一次。关闭|r| r.width
不会捕获、更改或从其环境中移出任何内容,因此
它满足 trait bound 要求。
相比之下,示例 13-8 展示了一个闭包的例子,它只实现了
这FnOnce
trait 的 trait 中,因为它将值移出环境。这
编译器不允许我们将这个闭包与sort_by_key
:
#[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:#?}");
}
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_operations
counter 的 ,因此可以称为 more
than once:
#[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"); }
FnMut
使用sort_by_key
允许这Fn
trait 在定义或使用
使用闭包。在下一节中,我们将讨论迭代器。多
Iterator 方法接受闭包参数,因此请牢记这些闭包细节
随着我们继续!
本文档由官方文档翻译而来,如有差异请以官方英文文档(https://doc.rust-lang.org/)为准