From e3acc2be0d9cb47a20bbaf39f6f62c423d486306 Mon Sep 17 00:00:00 2001 From: sunface Date: Wed, 22 Dec 2021 22:08:41 +0800 Subject: [PATCH] =?UTF-8?q?add=E9=97=AD=E5=8C=85?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/advance/functional-programing/closure.md | 371 +++++++++++++++++++ 1 file changed, 371 insertions(+) diff --git a/src/advance/functional-programing/closure.md b/src/advance/functional-programing/closure.md index 3b77244a..47f0967c 100644 --- a/src/advance/functional-programing/closure.md +++ b/src/advance/functional-programing/closure.md @@ -254,6 +254,7 @@ where } } + // 先查询缓存值`self.value`,若不存在,则调用`query`加载 fn value(&mut self, arg: u32) -> u32 { match self.value { Some(v) => v, @@ -267,4 +268,374 @@ where } ``` +上面的缓存有一个很大的问题:只支持`u32`类型的值,若我们想要缓存`String`类型,显然就行不通了,因此需要将`u32`替换成泛型`E`,该练习就留给读者自己完成,具体代码可以参考[这里](https://github.com/sunface/rust-course/blob/main/course-solutions/closure.md) + + +## 捕获作用域中的值 +在之前代码中,我们一直在用闭包的匿名函数特性(赋值给变量),然而闭包还拥有一项函数所不具备的特性: 捕获作用域中的值。 +```rust +fn main() { + let x = 4; + + let equal_to_x = |z| z == x; + + let y = 4; + + assert!(equal_to_x(y)); +} +``` + +上面代码中,`x`并不是闭包`equal_to_x`的参数,但是它依然可以去使用`x`,因为`x`在`equal_to_x`的作用域范围内。 + +对于函数来说,就算你把函数定义在`main`函数体中,它也不能访问`x`: +```rust +fn main() { + let x = 4; + + fn equal_to_x(z: i32) -> bool { + z == x + } + + let y = 4; + + assert!(equal_to_x(y)); +} +``` + +报错如下: +```console +error[E0434]: can't capture dynamic environment in a fn item // 在函数中无法捕获动态的环境 + --> src/main.rs:5:14 + | +5 | z == x + | ^ + | + = help: use the `|| { ... }` closure form instead // 使用闭包替代 +``` + +如上所示,编译器准确的告诉了我们错误,同时甚至给出了提示:使用闭包来替代函数,这种聪明令我有些无所适从,总感觉会显得我很笨。 + +#### 闭包对内存的影响 +当闭包从环境中捕获一个值时,会分配内存去存储这些值。对于有些场景来说,这种额外的内存分配会成为一种负担。与之相比,函数就不会去捕获这些环境值,因此定义和使用函数不会拥有这种内存负担。 + +#### 三种Fn特征 +闭包捕获变量有三种途径,恰好对应函数参数的三种传入方式:转移所有权、可变借用、不可变借用,因此相应的Fn特征也有三种: +1. `FnOnce`, 该类型的闭包会拿走被捕获变量的所有权。`Once`顾名思义,说明该闭包只能拿走所有权一次: +```rust +fn main() { + let x = vec![1,2,3]; + + let len_is = move |z| z == x.len(); + let len_is_not = move |z| z != x.len(); + println!("{}",len_is(3)); + println!("{}",len_is_not(4)); +} +``` + +`move`关键字用来告诉闭包,捕获变量的所有权而不是进行借用,因此`x`变量的所有权将被首选转移到`len_is`中,紧接着`len_is_not`又试图获取`x`的所有权,此时自然会报错: +```console +error[E0382]: use of moved value: `x` // 使用已经没有所有权的x + --> src/main.rs:5:22 + | +2 | let x = vec![1,2,3]; + | - move occurs because `x` has type `Vec`, which does not implement the `Copy` trait +3 | - 发生所有权转移因为x的类型是Vec,它没有实现Copy特征 +4 | let len_is = move |z| z == x.len(); + | -------- - variable moved due to use in closure // 在此处x所有权被转移 + | | + | value moved into closure here +5 | let len_is_not = move |z| z != x.len(); + | ^^^^^^^^ - use occurs due to use in closure + | | + | value used here after move // 试图再次转移所有权 +``` + +这里面有一个很重要的提示,因为`Vec`没有实现`Copy`特征,所以会报错,那么我们试试有实现`Copy`的类型: +```rust +fn main() { + let x = 3; + + let equal_to = move |z| z == x; + let not_equal_to = move |z| z != x; + println!("{}",equal_to(3)); + println!("{}",not_equal_to(4)); +} +``` + +上面代码中,`x`的类型是`i32`,该类型实现了`Copy`特征,因此虽然我们使用了`move`关键字让闭包拿走`x`的所有权,但是由于`x`是可复制的,闭包仅仅是复制了`x`的值,编译后,顺利通过: +```console +true +true +``` + +2. `FnMut`, 它以可变借用的方式捕获了环境中的值,因此可以修改该值: +```rust +fn main() { + let mut s = String::new(); + + let update_string = |str| s.push_str(str); + update_string("hello"); + + println!("{:?}",s); +} +``` + +在闭包中,我们调用`s.push_str`去改变外部`s`的字符串值,因此这里捕获了它的可变借用,运行下试试: +```console +error[E0596]: cannot borrow `update_string` as mutable, as it is not declared as mutable + --> src/main.rs:5:5 + | +4 | let update_string = |str| s.push_str(str); + | ------------- - calling `update_string` requires mutable binding due to mutable borrow of `s` + | | + | help: consider changing this to be mutable: `mut update_string` +5 | update_string("hello"); + | ^^^^^^^^^^^^^ cannot borrow as mutable +``` + +虽然报错了,但是编译器给出了非常清晰的提示,想要在闭包内部捕获可变借用,需要把该闭包声明为可变类型,也就是`update_string`要修改为`mut update_string`: +```rust +fn main() { + let mut s = String::new(); + + let mut update_string = |str| s.push_str(str); + update_string("hello"); + + println!("{:?}",s); +} +``` + +这种写法有点反直觉,相比起来前面的`move`更符合使用和阅读习惯。但是如果你忽略`update_string`的类型,仅仅把它当成一个普通变量,那么这种声明就比较合理了。 + +再来看一个复杂点的: +```rust +fn main() { + let mut s = String::new(); + + let update_string = |str| s.push_str(str); + + exec(update_string); + + println!("{:?}",s); +} + +fn exec<'a, F: FnMut(&'a str)>(mut f: F) { + f("hello") +} +``` + +这段代码非常清晰的说明了`update_string`实现了`FnMut`特征 + +3. `Fn`特征,它以不可变借用的方式捕获环境中的值 +让我们把上面的代码中`exec`的`F`泛型参数类型修改为`Fn(&'a str)`,然后运行看看结果: +```console +error[E0525]: expected a closure that implements the `Fn` trait, but this closure only implements `FnMut` + --> src/main.rs:4:26 // 期望闭包实现的是`Fn`特征,但是它只实现了`FnMut`特征 + | +4 | let update_string = |str| s.push_str(str); + | ^^^^^^-^^^^^^^^^^^^^^ + | | | + | | closure is `FnMut` because it mutates the variable `s` here + | this closure implements `FnMut`, not `Fn` //闭包实现的是FnMut,而不是Fn +5 | +6 | exec(update_string); + | ---- the requirement to implement `Fn` derives from here +``` + +从报错中很清晰的看出,我们的闭包实现的是`FnMut`特征,但是在`exec`中却给它标注了`Fn`特征,因此产生了不匹配,再来看看正确的不可变借用方式: +```rust +fn main() { + let s = "hello, ".to_string(); + + let update_string = |str| println!("{},{}",s,str); + + exec(update_string); + + println!("{:?}",s); +} + +fn exec<'a, F: Fn(String) -> ()>(f: F) { + f("world".to_string()) +} +``` + +在这里,因为无需改变`s`,因此闭包中只对`s`进行了不可变借用,那么在`exec`中,将其标记为`Fn`特征就完全正确。 + + +##### move和Fn +在上面,我们讲到了`move`关键字对于`FnOnce`特征的重要性,但是实际上使用了`move`的闭包依然可能实现了`Fn`或`FnMut`特征。 + +因为,**一个闭包实现了哪种Fn特征取决于该闭包如何使用被捕获的变量,而不是取决于闭包如何捕获它们**。`move`本身强调的就是后者:闭包如何捕获变量: +```rust +fn main() { + let s = String::new(); + + let update_string = move || println!("{}",s); + + exec(update_string); +} + +fn exec(f: F) { + f() +} +``` + +我们在上面的闭包中使用了`move`关键字,因此我们的闭包捕获了它,但是由于闭包对`s`的使用仅仅是不可变借用,因为该闭包实际上**还**实现了`Fn`特征,如`exec`函数所示。 + +细心的读者肯定发现我在上段中使用了一个`还`字,这是什么意思呢?因为该闭包不仅仅实现了`Fn`特征,还实现了`FnOnce`特征,因此将代码修改成下面这样,依然可以编译: +```rust +fn main() { + let s = String::new(); + + let update_string = move || println!("{}",s); + + exec(update_string); +} + +fn exec(f: F) { + f() +} +``` + +##### 三种Fn的关系 +实际上,一个闭包并不仅仅实现某一种Fn特征,规则如下: +- 所有的闭包都实现了`FnOnce`特征,因此任何一个闭包都至少可以被调用一次 +- 没有使用`move`的闭包实现了`FnMut`特征 +- 不需要对捕获变量进行改变的闭包实现了`Fn`特征 + +用一段代码来简单诠释上述规则: +```rust +fn main() { + let s = String::new(); + + let update_string = || println!("{}",s); + + exec(update_string); + exec1(update_string); + exec2(update_string); +} + +fn exec(f: F) { + f() +} + +fn exec1(mut f: F) { + f() +} + +fn exec2(f: F) { + f() +} +``` + +虽然,闭包只是对`s`进行了不可变借用,实际上,它可以适用于任何一种`Fn`特征:三个`exec`函数说明了一切。强烈建议读者亲自动手试试各种情况下使用的`Fn`特征,更有助于加深这方面的理解。 + +如果还是有疑惑?没关系,我们来看看这三个特征的简化版源码: +```rust +pub trait Fn : FnMut { + extern "rust-call" fn call(&self, args: Args) -> Self::Output; +} + +pub trait FnMut : FnOnce { + extern "rust-call" fn call_mut(&mut self, args: Args) -> Self::Output; +} + +pub trait FnOnce { + type Output; + + extern "rust-call" fn call_once(self, args: Args) -> Self::Output; +} +``` + +看到没?从特征约束能看出来`Fn`的前提是实现`FnMut`,`FnMut`的前提是实现`FnOne`,因此要实现`Fn`就要同时实现`FnMut`和`FnOnce`,这段源码从侧面印证了之前规则的正确性。 + +从源码中还能看出一点:`Fn`获取`&self`,`FnMut`获取`&mut self`,而`FnOnce`获取`self`. +在实际项目中,**建议先使用`Fn`特征**,然后编译器会告诉你正误以及该如何选择。 + +## 闭包作为函数返回值 +看到这里,相信大家对于如何使用闭包作为函数参数,已经很熟悉了,但是如果要使用闭包作为函数返回值,该如何做? + +先来看一段代码: +```rust +fn factory() -> Fn(i32) -> i32 { + let num = 5; + + |x| x + num +} + +let f = factory(); + +let answer = f(1); +assert_eq!(6, answer); +``` + +上面这段代码看起来还是蛮正常的,用`Fn(i32) -> i32`特征来代表`|x| x + num`,非常合理嘛,肯定可以编译通过, 可惜理想总是难以照进现实,编译器给我们报了一大堆错误,先挑几个重点来看看: +```console +fn factory() -> Fn(i32) -> i32 { + | ^^^^^^^^^^^^^^ doesn't have a size known at compile-time // 该类型在编译器没有固定的大小 +``` + +Rust要求函数的参数和返回类型,必须有固定的内存大小,例如`i32`就是4个字节,引用类型是8个字节,总之,绝大部分类型都有固定的大小,但是不包括特征,因为特征类似接口,对于编译器来说,无法知道它后面藏的真实类型是什么,因为也无法得知具体的大小。 + +但是我们又无法知道闭包的具体类型,该怎么办呢?再看看报错提示: +```console +help: use `impl Fn(i32) -> i32` as the return type, as all return paths are of type `[closure@src/main.rs:11:5: 11:21]`, which implements `Fn(i32) -> i32` + | +8 | fn factory() -> impl Fn(i32) -> i32 { +``` + +嗯,编译器提示我们加一个`impl`关键字,哦,这样一说,读者可能就想起来了,`impl Trait`可以用来返回一个实现了指定特征的类型,那么这里`impl Fn(i32) -> i32`的返回值形式,说明我们要返回一个闭包类型,它实现了`Fn(i32) -> i32`特征。 + +完美解决,但是,在[特征]那一章,我们提到过,`impl Trait`的返回方式有一个非常大的局限,就是你只能返回同样的类型,例如: +```rust +fn factory(x:i32) -> impl Fn(i32) -> i32 { + + let num = 5; + + if x > 1{ + move |x| x + num + } else { + move |x| x - num + } +} +``` +运行后,编译器报错: +```console +error[E0308]: `if` and `else` have incompatible types + --> src/main.rs:15:9 + | +12 | / if x > 1{ +13 | | move |x| x + num + | | ---------------- expected because of this +14 | | } else { +15 | | move |x| x - num + | | ^^^^^^^^^^^^^^^^ expected closure, found a different closure +16 | | } + | |_____- `if` and `else` have incompatible types + | +``` + +嗯,提示很清晰:`if`和`else`分支中返回了不同的闭包类型,这就很奇怪了,明明这两个闭包长的一样的,好在细心的读者应该回想起来,本章节前面咱们有提到:就算签名一样的闭包,类型也是不同的,因此在这种情况下,就无法再使用`impl Trait`的方式去返回闭包。 + +怎么办?再看看编译器提示,里面有这样一行小字: +```console += help: consider boxing your closure and/or using it as a trait object +``` + +哦,相信你已经恍然大悟,可以用特征对象!只需要用`Box`的方式即可实现: +```rust +fn factory(x:i32) -> Box i32> { + let num = 5; + + if x > 1{ + Box::new(move |x| x + num) + } else { + Box::new(move |x| x - num) + } +} +``` + +至此,闭包作为函数返回值就已完美解决,若以后你再遇到报错时,一定要仔细阅读编译器的提示,很多时候,转角都能遇到爱。 + ## 闭包的生命周期 +这块儿内容在进阶生命周期章节中有讲,这里就不再赘述,读者可移步[此处](https://course.rs/advance/lifetime/advance.html#闭包函数的消除规则)进行回顾。 \ No newline at end of file