update ch13-01

This commit is contained in:
KaiserY 2024-08-15 15:37:34 +08:00
parent f39b1fda92
commit ef254fbe96

View File

@ -32,15 +32,15 @@ Rust 的 **闭包***closures*)是可以保存在变量中或作为参数传
{{#include ../listings/ch13-functional-features/listing-13-01/output.txt}}
```
这里一个有趣的地方是我们传递了一个会在当前 `Inventory` 实例上调用 `self.most_stocked()` 的闭包。标准库并不需要知道我们定义的 `Inventory``ShirtColor` 类型或是在这个场景下我们想要用的逻辑。闭包捕获了一个 `Inventory` 实例的不可变引用到 `self`,并连同其它代码传递给 `unwrap_or_else` 方法。相比之下,函数就不能以这种方式捕获其环境。
这里一个有趣的地方是我们传递了一个闭包,该闭包会在当前 `Inventory` 实例上调用 `self.most_stocked()` 方法。标准库不需要了解我们定义的 `Inventory``ShirtColor` 类型,也不需要了解我们在这个场景中要使用的逻辑。闭包捕获了对 `self`(即 `Inventory` 实例)的不可变引用,并将其与我们指定的代码一起传递给 `unwrap_or_else` 方法。相比之下,函数无法以这种方式捕获其环境。
### 闭包类型推断和注解
函数与闭包还有更多区别。闭包并不总是要求像 `fn` 函数那样在参数和返回值上注明类型。函数中需要类型注解是因为它们是暴露给用户的显式接口的一部分。严格定义这些接口对保证所有人都对函数使用和返回值的类型理解一致是很重要的。与此相比,闭包并不用于这样暴露在外的接口:它们储存在变量中并被使用,不用命名它们或暴露给库的用户调用。
函数与闭包还有更多区别。闭包通常不要求像 `fn` 函数那样对参数和返回值进行类型注解。函数需要类型注解是因为这些类型是暴露给用户的显式接口的一部分。严格定义这些接口对于确保所有人对函数使用和返回值的类型达成一致理解非常重要。与此相比,闭包并不用于这样暴露在外的接口:它们储存在变量中并被使用,不用命名它们或暴露给库的用户调用。
闭包通常很短,并只关联于小范围的上下文而非任意情境。在这些有限制的上下文中,编译器能可靠地推断参数和返回值的类型,类似于它是如何能够推断大部分变量的类型一样(同时也有编译器需要闭包类型注解的罕见情况)。
闭包通常较短,并且只与特定的上下文相关,而不是适用于任意情境。在这些有限的上下文中,编译器可以推断参数和返回值的类型,类似于它推断大多数变量类型的方式(尽管在某些罕见的情况下,编译器也需要闭包的类型注解)。
类似于变量,如果我们希望增加明确性和清晰度也可以添加类型标注,坏处是使代码变得更啰嗦(相对于严格必要的代码)。为示例 13-1 中定义的闭包标注类型看起来如示例 13-2 中的定义一样。这个例子中,我们定义了一个闭包并将它保存在变量中,而不是像示例 13-1 那样在传参的地方定义它
类似于变量,如果我们希望增加代码的明确性和清晰度,可以添加类型注解,但代价是是会使代码变得比严格必要的更冗长。为示例 13-1 中定义的闭包标注类型看起来如示例 13-2 中的定义一样。这个例子中,我们定义了一个闭包并将它保存在变量中,而不是像示例 13-1 那样在传参的地方定义它
<span class="filename">文件名src/main.rs</span>
@ -50,7 +50,7 @@ Rust 的 **闭包***closures*)是可以保存在变量中或作为参数传
<span class="caption">示例 13-2为闭包的参数和返回值增加可选的类型注解</span>
有了类型注解闭包的语法就更类似函数了。如下是一个对其参数加一的函数的定义与拥有相同行为闭包语法的纵向对比。这里增加了一些空格来对齐相应部分。这展示了除了使用竖线以及一些可选语法外,闭包语法与函数语法有多么地相似:
有了类型注解,闭包的语法看起来就更像函数的语法了。如下是一个对其参数加一的函数的定义与拥有相同行为闭包语法的纵向对比。这里增加了一些空格来对齐相应部分。这展示了除了使用竖线以及一些可选语法外,闭包语法与函数语法有多么地相似:
```rust,ignore
fn add_one_v1 (x: u32) -> u32 { x + 1 }
@ -59,9 +59,9 @@ let add_one_v3 = |x| { x + 1 };
let add_one_v4 = |x| x + 1 ;
```
第一行展示了一个函数定义,第二行展示了一个完整标注的闭包定义。第三行闭包定义中省略了类型注解,而第四行去掉了可选的大括号,因为闭包体只有一个表达式。这些都是有效的闭包定义,并在调用时产生相同的行为。调用闭包是 `add_one_v3``add_one_v4` 能够编译的必要条件,因为类型将从其用法中推断出来。这类似于 `let v = Vec::new();`Rust 需要类型注解或是某种类型的值被插入到 `Vec` 才能推断其类型。
第一行展示了一个函数定义,第二行展示了一个完整标注的闭包定义。第三行闭包定义中省略了类型注解,而第四行去掉了可选的大括号,因为闭包体只有一个表达式,所以大括号是可选的。这些都是有效的闭包定义,并在调用时产生相同的行为。调用闭包是 `add_one_v3``add_one_v4` 能够编译的必要条件,因为类型将从其用法中推断出来。这类似于 `let v = Vec::new();`Rust 需要类型注解或是某种类型的值被插入到 `Vec` 中,才能推断其类型。
编译器会为闭包定义中的每个参数和返回值推断一个具体类型。例如,示例 13-3 中展示了仅仅将参数作为返回值的简短的闭包定义。除了作为示例的目的这个闭包并不是很实用。注意这个定义没有增加任何类型注解,所以我们可以用任意类型来调用这个闭包。但是如果尝试调用闭包两次,第一次使用 `String` 类型作为参数而第二次使用 `u32`,则会得到一个错误:
对于闭包定义,编译器会为每个参数和返回值推断出一个具体类型。例如,示例 13-3 展示了一个简短的闭包定义,该闭包仅仅返回作为参数接收到的值。除了作为示例用途外,这个闭包并不是很实用。注意这个定义没有增加任何类型注解。因为没有类型注解,我们可以使用任意类型来调用这个闭包,我们在这里第一次调用时使用了 `String` 类型。但是如果我们接着尝试使用整数来调用 `example_closure`,就会得到一个错误。
<span class="filename">文件名src/main.rs</span>
@ -77,11 +77,11 @@ let add_one_v4 = |x| x + 1 ;
{{#include ../listings/ch13-functional-features/listing-13-03/output.txt}}
```
第一次使用 `String` 值调用 `example_closure` 时,编译器推断这个闭包中 `x` 的类型以及返回值的类型是 `String`。接着这些类型被锁定进闭包 `example_closure` 中,如果尝试对同一闭包使用不同类型则就会得到类型错误。
第一次使用 `String` 值调用 `example_closure` 时,编译器推断出 `x` 的类型以及闭包的返回类型为 `String`。接着这些类型被锁定进闭包 `example_closure` 中,如果尝试对同一闭包使用不同类型则就会得到类型错误。
### 捕获引用或者移动所有权
闭包可以通过三种方式捕获其环境,它们直接对应到函数获取参数的三种方式:不可变借用,可变借用和获取所有权。闭包会根据函数体中如何使用被捕获的值决定用哪种方式捕获
闭包可以通过三种方式捕获其环境中的值,它们直接对应到函数获取参数的三种方式:不可变借用、可变借用和获取所有权。闭包将根据函数体中对捕获值的操作来决定使用哪种方式
在示例 13-4 中定义了一个捕获名为 `list` 的 vector 的不可变引用的闭包,因为只需不可变引用就能打印其值:
@ -93,9 +93,9 @@ let add_one_v4 = |x| x + 1 ;
<span class="caption">示例 13-4定义并调用一个捕获不可变引用的闭包</span>
这个示例也展示了变量可以绑定一个闭包定义,并且之后可以使用变量名和括号来调用闭包,就像变量名是函数名一样
这个示例也展示了变量可以绑定一个闭包定义,并且我们可以像使用函数名一样,使用变量名和括号来调用该闭包
因为同时可以有多个 `list` 的不可变引用,所以在闭包定义之前,闭包定义之后调用之前,闭包调用之后代码仍然可以访问 `list`代码可以编译、运行并打印
因为同时可以有多个 `list` 的不可变引用,所以在闭包定义之前,闭包定义之后调用之前,闭包调用之后代码仍然可以访问 `list`该代码可以编译、运行并输出
```console
{{#include ../listings/ch13-functional-features/listing-13-04/output.txt}}
@ -117,11 +117,11 @@ let add_one_v4 = |x| x + 1 ;
{{#include ../listings/ch13-functional-features/listing-13-05/output.txt}}
```
注意在 `borrows_mutably` 闭包的定义和调用之间不再有 `println!`,当 `borrows_mutably` 定义时,它捕获了 `list` 的可变引用。闭包在被调用后就不再被使用,这时可变借用结束。因为当可变借用存在时不允许有其它的借用,所以在闭包定义和调用之间不能有不可变引用来进行打印。可以尝试在这里添加 `println!` 看看你会得到什么报错信息!
注意在 `borrows_mutably` 闭包的定义和调用之间不再有 `println!`这是因为`borrows_mutably` 定义时,它捕获了 `list` 的可变引用。闭包在被调用后就不再被使用,这时可变借用结束。因为当可变借用存在时不允许有其它的借用,所以在闭包定义和调用之间不能有不可变引用来进行打印。可以尝试在这里添加 `println!` 看看你会得到什么报错信息!
即使闭包体不严格需要所有权,如果希望强制闭包获取它用到的环境中值的所有权,可以在参数列表前使用 `move` 关键字。
即使闭包体不严格需要所有权,如果希望强制闭包获取它在环境中所使用的值的所有权,可以在参数列表前使用 `move` 关键字。
在将闭包传递到一个新的线程时这个技巧很有用,它可以移动数据所有权给新线程。我们将在 16 章讨论并发时详细讨论线程以及为什么你想要使用它们。现在我们先简单探讨用 `move` 关键字的闭包来生成新的线程。示例 13-6 修改了示例 13-4 以便在一个新的线程而非主线程中打印 vector
当将闭包传递到一个新的线程时,这个技巧特别有用,因为它将数据的所有权移动到新线程中。我们将在第十六章讨论并发时详细讨论线程以及为什么你可能需要使用它们。不过现在,我们先简要探索一下如何使用需要 `move` 关键字的闭包来生成一个新线程。示例 13-6 展示了如何修改示例 13-4以便在一个新线程中而不是在主线程中打印 vector
<span class="filename">文件名src/main.rs</span>
@ -131,7 +131,7 @@ let add_one_v4 = |x| x + 1 ;
<span class="caption">示例 13-6使用 `move` 来强制闭包为线程获取 `list` 的所有权</span>
我们生成了新的线程,给这个线程一个闭包作为参数运行,闭包体打印出列表。在示例 13-4 中,闭包通过不可变引用捕获 `list`,因为这是打印列表所需的最少的访问。这个例子中,尽管闭包体依然只需要不可变引用,我们还是在闭包定义前写上 `move` 关键字来指明 `list` 应当被移动到闭包中。新线程可能在主线程剩余部分执行完前执行完,或者也可能主线程先执行完。如果主线程维护了 `list` 的所有权但却在新线程之前结束并且丢弃了 `list`,则在线程中的不可变引用将失效。因此,编译器要求 `list` 被移动到在新线程中运行的闭包中,这样引用就是有效的。试着去掉 `move` 关键字或在闭包被定义后在主线程中使用 `list` 看看你会得到什么编译器报错!
我们生成了一个新的线程,给这个线程传递一个闭包作为参数运行,闭包体打印出列表。在示例 13-4 中,闭包通过不可变引用捕获 `list`,因为这是打印列表所需的最少的访问权限。这个例子中,尽管闭包体依然只需要不可变引用,我们还是在闭包定义前写上 `move` 关键字,以确保 `list` 被移动到闭包中。新线程可能在主线程剩余部分执行完前执行完,或者也可能主线程先执行完。如果主线程维护了 `list` 的所有权但却在新线程之前结束并且丢弃了 `list`,则在线程中的不可变引用将失效。因此,编译器要求 `list` 被移动到在新线程中运行的闭包中,这样引用就是有效的。试着移除 `move` 关键字,或者在闭包定义后在主线程中使用 `list`看看你会得到什么编译器报错!
<a id="storing-closures-using-generic-parameters-and-the-fn-traits"></a>
<a id="limitations-of-the-cacher-implementation"></a>
@ -163,15 +163,15 @@ impl<T> Option<T> {
}
```
回忆 `T` 是表示 `Option``Some` 成员中的值的类型的泛型。类型 `T` 也是 `unwrap_or_else` 函数的返回值类型:举例来说,在 `Option<String>` 上调用 `unwrap_or_else` 会得到一个 `String`
回忆一下,`T` 是表示 `Option``Some` 成员中的值的类型的泛型。类型 `T` 也是 `unwrap_or_else` 函数的返回值类型:举例来说,在 `Option<String>` 上调用 `unwrap_or_else` 会得到一个 `String`
接着注意到 `unwrap_or_else` 函数有额外的泛型参数 `F``F``f` 参数(即调用 `unwrap_or_else` 时提供的闭包)的类型
接着注意到 `unwrap_or_else` 函数有额外的泛型参数 `F`。`F` 是参数 `f` 的类型,`f` 是调用 `unwrap_or_else` 时提供的闭包。
泛型 `F` 的 trait bound 是 `FnOnce() -> T`,这意味着 `F` 必须能够被调用一次,没有参数并返回一个 `T`。在 trait bound 中使用 `FnOnce` 表示 `unwrap_or_else` 最多调用 `f` 一次。在 `unwrap_or_else` 的函数体中可以看到,如果 `Option``Some``f` 不会被调用。如果 `Option``None``f` 将会被调用一次。由于所有的闭包都实现了 `FnOnce``unwrap_or_else` 能接收绝大多数不同类型的闭包,十分灵活。
泛型 `F` 的 trait bound 是 `FnOnce() -> T`,这意味着 `F` 必须能够被调用一次,没有参数并返回一个 `T`。在 trait bound 中使用 `FnOnce` 表示 `unwrap_or_else` 最多只会调用 `f` 一次。在 `unwrap_or_else` 的函数体中可以看到,如果 `Option``Some``f` 不会被调用。如果 `Option``None``f` 将会被调用一次。由于所有的闭包都实现了 `FnOnce``unwrap_or_else` 接受所有三种类型的闭包,十分灵活。
> 注意:函数也可以实现所有的三种 `Fn` traits。如果我们要做的事情不需要从环境中捕获值则可以在需要某种实现了 `Fn` trait 的东西时使用函数而不是闭包。举个例子,可以在 `Option<Vec<T>>` 的值上调用 `unwrap_or_else(Vec::new)` 以便在值为 `None` 时获取一个新的空的 vector。
> 注意:函数也可以实现所有的三种 `Fn` traits。如果我们要做的事情不需要从环境中捕获值则可以在需要某种实现了 `Fn` trait 的东西时使用函数而不是闭包。举个例子,可以在 `Option<Vec<T>>` 的值上调用 `unwrap_or_else(Vec::new)`以便在值为 `None` 时获取一个新的空的 vector。
现在让我们来看定义在 slice 上的标准库方法 `sort_by_key`,看看它与 `unwrap_or_else` 的区别以及为什么 `sort_by_key` 使用 `FnMut` 而不是 `FnOnce` trait bound。这个闭包以一个 slice 中当前被考虑的元素的引用作为参数,返回一个可以用来排序的 `K` 类型的值。当你想按照 slice 中元素的某个属性来进行排序时这个函数很有用。在示例 13-7 中有一个 `Rectangle` 实例的列表,我们使用 `sort_by_key``Rectangle``width` 属性对它们从低到高排序:
现在让我们来看定义在 slice 上的标准库方法 `sort_by_key`,看看它与 `unwrap_or_else` 的区别以及为什么 `sort_by_key` 使用 `FnMut` 而不是 `FnOnce` 作为 trait bound。这个闭包以一个 slice 中当前被考虑的元素的引用作为参数,返回一个可以排序的 `K` 类型的值。当你想按照 slice 中每个元素的某个属性进行排序时,这个函数非常有用。在示例 13-7 中,我们有一个 `Rectangle` 实例的列表,并使用 `sort_by_key``Rectangle``width` 属性对它们从低到高排序:
<span class="filename">文件名src/main.rs</span>
@ -187,9 +187,9 @@ impl<T> Option<T> {
{{#include ../listings/ch13-functional-features/listing-13-07/output.txt}}
```
`sort_by_key` 被定义为接收一个 `FnMut` 闭包的原因是它会多次调用这个闭包:每个 slice 中的元素调用一次。闭包 `|r| r.width` 不捕获、修改或将任何东西移出它的环境,所以它满足 trait bound 的要求。
`sort_by_key` 被定义为接收一个 `FnMut` 闭包的原因是它会多次调用这个闭包:对 slice 中的每个元素调用一次。闭包 `|r| r.width` 不捕获、修改或将任何东西移出它的环境,所以它满足 trait bound 的要求。
作为对比,示例 13-8 展示了一个只实现了 `FnOnce` trait 的闭包(因为它从环境中移出了一个值)的例子。编译器不允许我们在 `sort_by_key`使用这个闭包:
相比之下,示例 13-8 展示了一个只实现了 `FnOnce` trait 的闭包的例子,因为它从环境中移出了一个值。编译器不允许我们在 `sort_by_key`使用这个闭包:
<span class="filename">文件名src/main.rs</span>
@ -199,13 +199,13 @@ impl<T> Option<T> {
<span class="caption">示例 13-8尝试在 `sort_by_key` 上使用一个 `FnOnce` 闭包</span>
这是一个刻意构造的、繁琐的方式,它尝试统计排序 `list``sort_by_key` 被调用的次数(并不能工作)。该代码尝试在闭包的环境中向 `sort_operations` vector 放入 `value`— 一个 `String` 来实现计数。闭包捕获了 `value` 然后通过转移 `value` 的所有权的方式将其移出闭包给到 `sort_operations` vector。这个闭包可以被调用一次尝试再次调用它将报错。因为这时 `value` 已经不在闭包的环境中,无法被再次放到 `sort_operations` 中!因而,这个闭包只实现了 `FnOnce`。由于要求闭包必须实现 `FnMut`,因此尝试编译这个代码将得到报错:`value` 不能被移出闭包
这是一个刻意构造的、复杂且无效的方式,试图统计在对 `list` 进行排序时 `sort_by_key` 调用闭包的次数。该代码试图通过将闭包环境中的 `value`(一个 `String`)插入 `sort_operations` vector 来实现计数。闭包捕获了 `value`,然后通过将 `value` 的所有权转移给 `sort_operations` vector 的方式将其移出闭包。这个闭包只能被调用一次;尝试第二次调用它将无法工作,因为这时 `value` 已经不在闭包的环境中,无法被再次插入 `sort_operations` 中!因而,这个闭包只实现了 `FnOnce`。当我们尝试编译此代码时,会出现错误提示:`value` 不能从闭包中移出,因为闭包必须实现 `FnMut`
```console
{{#include ../listings/ch13-functional-features/listing-13-08/output.txt}}
```
报错指向了闭包体中将 `value` 移出环境的那一行。要修复这里,我们需要改变闭包体让它不将值移出环境。在环境中保持一个计数器并在闭包体中增加它的值是计算 `sort_by_key` 被调用次数的一个更简单直接的方法。示例 13-9 中的闭包可以在 `sort_by_key` 中使用,因为它只捕获了 `num_sort_operations` 计数器的可变引用,这就可以被调用多次。
报错指向了闭包体中将 `value` 移出环境的那一行。要修复此问题,我们需要修改闭包体,使其不会将值移出环境。在环境中维护一个计数器,并在闭包体中递增其值,是计算闭包被调用次数的一个更简单直接的方法。示例 13-9 中的闭包可以在 `sort_by_key` 中使用,因为它只捕获了 `num_sort_operations` 计数器的可变引用,因此可以被多次调用:
<span class="filename">文件名src/main.rs</span>
@ -215,6 +215,6 @@ impl<T> Option<T> {
<span class="caption">示例 13-9允许在 `sort_by_key` 上使用一个 `FnMut` 闭包</span>
当定义或使用用到闭包的函数或类型时,`Fn` trait 十分重要。在下个小节中,我们将会讨论迭代器。许多迭代器方法都接收闭包参数,因而在继续前先记住这些闭包的细节!
当定义或使用涉及闭包的函数或类型时,`Fn` traits 十分重要。在下个小节中,我们将讨论迭代器。许多迭代器方法都接收闭包参数,因此在继续前,请记住这些闭包的细节!
[unwrap-or-else]: https://doc.rust-lang.org/std/option/enum.Option.html#method.unwrap_or_else