学习笔记 20240730 Rust语言-闭包三种Fn特征

20240730

闭包捕获变量有三种途径,恰好对应函数参数的三种传入方式:转移所有权、可变借用、不可变借用,因此相应的 Fn 特征也有三种:FnOnce,FnMut,Fn

所以本文主要有两种所有权的概念,闭包捕获变量的所有权和闭包的所有权,其中闭包的所有权一般在闭包作为函数参数时有所涉及,在正文开始前,先对闭包捕获变量的所有权进行学习

写在前面:在 Rust 语言中,闭包(closure)可以捕获其环境变量的值。这种捕获可以通过两种方式实现:借用(borrowing)或获取所有权(taking ownership)。具体使用哪种方式取决于闭包是如何定义的。

闭包所捕获变量的所有权

1.借用

.当闭包借用其环境变量时,它不会获取这些变量的所有权,因此这些变量在闭包执行完毕后仍然可用。
.借用可以是不可变(immutable)的或可变(mutable)的。
.闭包的参数类型会决定其如何捕获变量。例如:

不可变借用:

fn main() {
    let x = "hello".to_string();
    let closure: impl Fn()= || {println!("{}", x);};
    closure();
    println!("{}", x); // 正常工作
}

可变借用:

fn main() {
    let mut x = "hello".to_string();
    let mut closure: impl FnMut() = || x.push_str("string");
    closure();
    println!("The value of x is {}", x); // 正常工作
}

2.获取所有权

.当闭包获取其环境变量的所有权时,这些变量的所有权被移动到闭包中,原始变量不再可用。
.这种方式通常用于那些不再需要在闭包外部访问的变量。
.闭包的参数类型也会决定其如何捕获变量。例如:

fn main() {
    let x = vec![1, 2, 3];
    let closure: impl FnOnce() = move || {
        println!("{:?}", x);
    };
    closure(); // 正常工作
    // println!("{:?}", x); 这行会报错,因为 x 的所有权已经被移动到闭包中
}

在这个例子中,闭包通过 move 关键字获取了 x 的所有权。这意味着在闭包执行后,原始的 x 将不再可用。

闭包作为函数参数以及三种Fn特征

1.FnOnce

该类型的闭包会拿走被捕获变量的所有权。Once 顾名思义,说明该闭包只能运行一次。注意:被捕获变量的所有权和闭包的所有权不是一回事。看下面例子:

fn fn_once<F>(func: F)
where
    F: FnOnce(usize) -> bool,
{
    println!("{}", func(3));
    println!("{}", func(4));
}

fn main() {
    let x = vec![1, 2, 3];
    fn_once(|z|{z == x.len()})
}

对代码解释说明:这段 Rust 代码定义了一个函数 fn_once,它接受一个参数 func,这个参数是一个闭包或函数,其类型为 FnOnce(usize) -> bool,即闭包的参数是usize,闭包返回bool类型。FnOnce 是 Rust 中的一种函数指针类型,表示一个函数或闭包,可以被调用一次。

实现 FnOnce 特征的闭包在调用时会转移闭包的所有权,所以显然不能对已失去所有权的闭包变量进行二次调用,这里的“仅”加粗注释了,在后面学习中可以知道,实际上,一个闭包并不仅仅实现某一种 Fn 特征,特此说明。

运行结果如下:

error[E0382]: use of moved value: `func`
 --> src\main.rs:6:20
  |
1 | fn fn_once<F>(func: F)
  |               ---- move occurs because `func` has type `F`, which does not implement the `Copy` trait
                  // 因为`func`的类型是没有实现`Copy`特性的 `F`,所以发生了所有权的转移
...
5 |     println!("{}", func(3));
  |                    ------- `func` moved due to this call // 转移在这
6 |     println!("{}", func(4));
  |                    ^^^^ value used here after move // 转移后再次用
  |

“use of moved value: func” 可以说明,在实现了FnOnce特征的闭包在调用时,确实是转移了所有权。

这里面有一个很重要的提示,因为 F 没有实现 Copy 特征,所以会报错,那么我们添加一个约束,试试实现了 Copy 的闭包:

fn fn_once<F>(func: F)
where
    F: FnOnce(usize) -> bool + Copy,// 改动在这里
{
    println!("{}", func(3));
    println!("{}", func(4));
}

fn main() {
    let x = vec![1, 2, 3];
    fn_once(|z|{z == x.len()})
}

上述改动能够实现正确运行,但是总觉得有点怪,我们声明了FnOnce特征,就是想实现所有权的调用,现在又添加了Copy特征,导致所有权并没有发生转移,而是变成简单的拷贝,所以这里声明FnOnce有点没必要的感觉,FnOnce和Copy有点矛盾的感觉。带着疑问可以继续往下学习。

此外,补充两种改动的方法:
使用FnMut改动:

fn fn_once<F>(mut func: F)
where
    F: FnMut(usize) -> bool ,
{
    println!("{}", func(3));
    println!("{}", func(4));
}

fn main() {
    let x = vec![1, 2, 3];
    fn_once(|z|{z == x.len()});
}

使用Fn改动:

fn fn_once<F>(func: F)
where
    F: Fn(usize) -> bool ,
{
    println!("{}", func(3));
    println!("{}", func(4));
}

fn main() {
    let x = vec![1, 2, 3];
    fn_once(|z|{z == x.len()});
}

PS:如果你想强制闭包取得捕获变量的所有权,可以在参数列表前添加 move 关键字,这种用法通常用于闭包的生命周期大于捕获变量的生命周期时,例如将闭包返回或移入其他线程。

fn main() {
    let x = vec![1, 2, 3];
    let closure = move || {
    println!("{:?}", x)
    };
    println!("{:?}",x);
}

执行代码会报错:

error[E0382]: borrow of moved value: `x`
 --> src/main.rs:7:21
  |
3 |     let x = vec![1, 2, 3];
  |         - move occurs because `x` has type `Vec<i32>`, which does not implement the `Copy` trait
4 |     let closure = move || {
  |                   ------- value moved into closure here
5 |     println!("{:?}", x);
  |                      - variable moved due to use in closure
6 | };
7 |     println!("{:?}",x);
  |                     ^ value borrowed here after move
  |
  = note: this error originates in the macro `$crate::format_args_nl` which comes from the expansion of the macro `println` (in Nightly builds, run with -Z macro-backtrace for more info)

2.FnMut

它以可变借用的方式捕获了环境中的值,因此可以修改该值.

fn main() {
    let mut s = String::new();

    let update_string =  |str| s.push_str(str);
    update_string("hello");

    println!("{:?}",s);
}

在闭包中,我们调用 s.push_str 去改变外部 s 的字符串值,因此这里捕获了它的可变借用,运行下试试:

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:

fn main() {
    let mut s = String::new();

    let mut update_string =  |str| s.push_str(str);
    update_string("hello");

    println!("{:?}",s);
}
"hello"

再来看一个复杂点的:

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没有使用mut关键字修饰,而上文提到想要在闭包内部捕获可变借用,需要用关键词把该闭包声明为可变类型。我们确实这么做了———exec(mut f: F)表明我们的exec接收的是一个可变类型的闭包。这段代码中update_string看似被声明为不可变闭包,但是exec(mut f: F)函数接收的又是可变参数,为什么可以正常执行呢?

rust不可能接受类型不匹配的形参和实参通过编译,我们提供的实参又是可变的,这说明update_string一定是一个可变类型的闭包,我们不妨看看rust-analyzer自动给出的类型标注:

let mut s: String = String::new();
let update_string: impl FnMut(&str) =  |str| s.push_str(str);

rust-analyzer给出的类型标注非常清晰的说明了 update_string 实现了 FnMut 特征。

为什么update_string没有用mut修饰却是一个可变类型的闭包?事实上,FnMut只是trait的名字,声明变量为FnMut和要不要mut没啥关系,FnMut是推导出的特征类型,mut是rust语言层面的一个修饰符,用于声明一个绑定是可变的。Rust从特征类型系统和语言修饰符两方面保障了我们的程序正确运行。

我们在使用FnMut类型闭包时需要捕获外界的可变借用,因此我们常常搭配mut修饰符使用。但我们要始终记住,二者是相互独立的。

因此,让我们再回头分析一下这段代码:在main函数中,首先创建了一个可变的字符串s,然后定义了一个可变类型闭包update_string,该闭包接受一个字符串参数并将其追加到s中。接下来调用了exec函数,并将update_string 闭包的所有权 移交给它。最后打印出了字符串s的内容。(说明:exec函数调用参数mut f,而这个f是F类型,F类型是 F: FnMut(&'a str),即闭包类型,f前面没有&,所以是取用闭包的所有权,而不是闭包的借用)

PS:注意区分闭包的所有权,和闭包所捕获变量的所有权,因为本文中有大量所有权的字眼,所以不要搞混

细心的读者可能注意到,我们在上文的分析中提到update_string闭包的所有权被移交给了exec函数。这说明update_string没有实现Copy特征,但并不是所有闭包都没有实现Copy特征,闭包自动实现Copy特征的规则是,只要闭包捕获的类型都实现了Copy特征的话,这个闭包就会默认实现Copy特征。

我们来看一个例子:

let s = String::new();
let update_string =  || println!("{}",s);

这里取得的是s的不可变引用,所以是能Copy的。说明:该闭包没有使用move关键字,并且该闭包的函数内容没有体现出对s的修改,rust-analyzer自动给出的类型标注是

let s: String = String::new();
let update_string: impl Fn() =  || println!("{}",s);

而如果拿到的是s的所有权或可变引用,都是不能Copy的。我们刚刚的代码就属于第二类,取得的是s的可变引用,没有实现Copy。注意,对同一个对象,可变引用同一时间只能出现一次,而不可变引用可出现多个。

再看以下例子:

fn exec<F: FnOnce()>(f: F)  {
    f()
}

fn exec1<F: FnMut()>(mut f: F)  {
    f()
}

fn exec2<F: Fn()>(f: F)  {
    f()
}
// 拿所有权
fn main() {
    let s = String::new();
    let update_string = move || println!("{}", s);

    exec(update_string);
//  exec2(update_string); // 不能再用了

//  可变引用
    let mut s = String::new();
    let mut update_string = || s.push_str("hello");
    exec(update_string);
//  exec1(update_string); // 不能再用了
}

3.Fn

它以不可变借用的方式捕获环境中的值

让我们把上面的代码中 exec 的 F 泛型参数类型修改为 Fn(&'a str):

fn main() {
    let mut s = String::new();

    let update_string =  |str| s.push_str(str);

    exec(update_string);

    println!("{:?}",s);
}

fn exec<'a, F: Fn(&'a str)>(mut f: F)  {
    f("hello")
}

然后运行看看结果:

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 特征,因此产生了不匹配,再来看看正确的不可变借用方式:

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 本身强调的就是后者,闭包如何捕获变量:

fn main() {
    let s = String::new();

    let update_string =  move || println!("{}",s);

    exec(update_string);
}

fn exec<F: FnOnce()>(f: F)  {
    f()
}

我们在上面的闭包中使用了 move 关键字,所以我们的闭包捕获了它,但是由于闭包对 s 的使用仅仅是不可变借用,因此该闭包实际上实现了 Fn 特征。

查看上述代码的rust-analyzer自动给出的类型标注:

fn main() {
    let s = String::new();

    let update_string:impl Fn() =  move || println!("{}",s);

    exec(update_string);
}

fn exec<F: FnOnce()>(f: F)  {
    f()
}

虽然用move捕获变量,实际上实现了Fn特征。

细心的读者肯定发现我在上上段中使用了一个 字,这是什么意思呢?因为该闭包不仅仅实现了 FnOnce 特征,还实现了 Fn 特征,将代码修改成下面这样,依然可以编译:

fn main() {
    let s = String::new();

    let update_string =  move || println!("{}",s);

    exec(update_string);
}

fn exec<F: Fn()>(f: F)  {
    f()
}

三种 Fn 的关系

实际上,一个闭包并不仅仅实现某一种 Fn 特征,规则如下:

.所有的闭包都自动实现了 FnOnce 特征,因此任何一个闭包都至少可以被调用一次
.没有移出所捕获变量的所有权的闭包自动实现了 FnMut 特征
.不需要对捕获变量进行改变的闭包自动实现了 Fn 特征

用一段代码来简单诠释上述规则:

fn main() {
    let s = String::new();

    let update_string =  || println!("{}",s);

    exec(update_string);
    exec1(update_string);
    exec2(update_string);
}

fn exec<F: FnOnce()>(f: F)  {
    f()
}

fn exec1<F: FnMut()>(mut f: F)  {
    f()
}

fn exec2<F: Fn()>(f: F)  {
    f()
}

虽然,闭包只是对 s 进行了不可变借用,实际上,它可以适用于任何一种 Fn 特征:三个 exec 函数说明了一切。

关于第二条规则,有如下示例:

fn main() {
    let mut s = String::new();

    let update_string = |str| -> String {s.push_str(str); s };

    exec(update_string);
}

fn exec<'a, F: FnMut(&'a str) -> String>(mut f: F) {
    f("hello");
}
5 |     let update_string = |str| -> String {s.push_str(str); s };
  |                         ^^^^^^^^^^^^^^^                   - closure is `FnOnce` because it moves the variable `s` out of its environment
  |                                                           // 闭包实现了`FnOnce`,因为它从捕获环境中移出了变量`s`
  |                         |
  |                         this closure implements `FnOnce`, not `FnMut`

此例中,闭包从捕获环境中移出了变量 s 的所有权,因此这个闭包仅自动实现了 FnOnce,未实现 FnMut 和 Fn。再次印证之前讲的一个闭包实现了哪种 Fn 特征取决于该闭包如何使用被捕获的变量,而不是取决于闭包如何捕获它们,跟是否使用 move 没有必然联系。

我们来看看这三个特征的简化版源码:

pub trait Fn<Args> : FnMut<Args> {
    extern "rust-call" fn call(&self, args: Args) -> Self::Output;
}

pub trait FnMut<Args> : FnOnce<Args> {
    extern "rust-call" fn call_mut(&mut self, args: Args) -> Self::Output;
}

pub trait FnOnce<Args> {
    type Output;

    extern "rust-call" fn call_once(self, args: Args) -> Self::Output;
}

从特征约束能看出来 Fn 的前提是实现 FnMut,FnMut 的前提是实现 FnOnce,因此要实现 Fn 就要同时实现 FnMut 和 FnOnce,这段源码从侧面印证了之前规则的正确性。

从源码中还能看出一点:Fn 获取 &self,FnMut 获取 &mut self,而 FnOnce 获取 self。 在实际项目中,建议先使用 Fn 特征,然后编译器会告诉你正误以及该如何选择。

参考文献

Rust语言圣经

  • 10
    点赞
  • 15
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值