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 特征,然后编译器会告诉你正误以及该如何选择。