Rust闭包小结
-
Fn
/FnMut
/FnOnce
闭包的本质是实现了上述三个call trait中某一个的匿名结构体,捕获环境变量指的是该匿名结构体对待环境变量的方式,可以只用immutale reference在匿名结构体中访问环境变量,也可以对环境变量通过一个mutable reference进行修改,也可以把环境变量消耗掉。首先看在单线程且不加move关键字的情况下,当闭包体内的代码访问环境变量时仅仅通过immutable reference,整个闭包的类型是
Fn()+Copy
,为啥实现了Copy呢?Rust中自动实现了Copy的类型一般是标量,而结构体和enum不自动实现Copy,要想实现Copy必须手动写 #[derive(Copy, Clone)]且所有字段都可以被实现Copy。当闭包内部用环境变量的方式是immutable reference时,这种类型的reference能够被Copy的。所以也许是为了方便,我们不写#[derive(Copy, Clone)],闭包也给我们实现了#[derive(Copy, Clone)]。而加了move之后,匿名结构体拥有被捕获变量的ownership,因此无法实现Copy这个trait。不同的闭包,其结构体不同,因此闭包类型是DST,当需要使用闭包作为参数时,可以用impl或者trait object或者泛型。
move
影响的是闭包在初始化时是否把环境变量move到了匿名结构体内。闭包体内如何使用环境变量影响了闭包实现哪种Trait,即影响了闭包自身的类型。
如何使用变量决定了闭包实现的是哪种Trait是什么意思?
- 闭包体只用不可变引用的方式访问环境变量,闭包就实现了Fn
- 闭包体用可变引用访问环境变量,闭包就实现了FnMut
- 闭包的方法若只能被调用一次,即实现了FnOnce。(说明闭包消耗了捕获的变量)。
Rust会根据闭包体进行环境变量的捕获,闭包捕获环境中变量的模式优先级顺序为:不可变借用,可变借用,所有权。
fn main() { // 下面这段代码的闭包体中它没有获得变量的所有权,没有转让变量的所有权, // 所以这个闭包的类型是Fn()->Box<String> // 在闭包捕获完s之后原来的变量仍然能够使用 let s = String::from("gef"); let t = || { let s = &s; let g = Box::new((*s).clone()); g }; println!("{}", s); // 这个闭包的闭包体和上面仅有一处不同,那就是加了move,这会强行拿走s的所有权,即影响了捕获方式。 // ****即使**** 闭包体看起来是用最普通的不可变引用来操作的环境中的变量,也看起来不该拿走变量的所有权 let s = String::from("gef"); let t = move || { let s = &s; let g = Box::new((*s).clone()); g }; // 不加注释会编译失败,s所有权已经被拿走了 // println!("{}",s); // 即使用了move,这个闭包的类型依然是Fn()->Box<String> let fuck:Box<dyn Fn()->Box<String>>=Box::new(t); let s=String::from("gef"); let t=||s; // 这行代码无法编译,因为s的所有权被移动到闭包里面去了,但如果闭包体是 &s,则可以编译 // println!("{}",s) // 无论用不用move,这个闭包的类型都是FnOnce()->String, 因为闭包实现哪种Trait只和如何使用捕获到的变量有关系 // 但是闭包体的代码在一定程度上影响了变量如何捕获,Rust会通过推导来决定要不要把变量的所有权转移到闭包体内 // 对于没有实现Copy Trait的类型来说,不加move的话,环境变量会不会被拿走所有权,取决于闭包体内的使用方式 // **但是如果加了move,无论闭包体怎么写,被捕获的环境变量(未实现Copy Trait)一定会被拿走所有权** }
闭包的初始化行为只会执行一次,就是在闭包定义处,而不是在闭包执行处。在这个过程中会进行变量的捕获,且仅在闭包的定义处捕获一次,后期执行处不再进行捕获操作。
fn main(){ let s=String::from("111"); let g=||s; g(); let g=||s; // 🔺 g(); // 🔺 }
即使把环境变量恢复了,闭包也不会再去捕获了,而在闭包定义处,变量已经被移动到闭包体内了,接着在第一次执行的时候变量被消耗掉了,第二次执行的时候不会再进行变量的捕获动作,所以变量就没了。所以二次执行闭包会报错。
从这里可以看出来,如果一个闭包只能执行一次,说明它肯定拿走了环境变量的所有权,所以这种闭包一定是FnOnce类型的。
事实上FnOnce是每个闭包都实现了的Trait,因为每个闭包都至少能执行一次,然后根据闭包使用变量的方式逐渐特化其实现的Trait。
如果闭包拿走了环境变量的所有权,导致不能二次执行,则这种闭包的类型一定是FnOnce.
但是导致不能二次执行的原因可能是闭包的move关键字。不过move关键字不会影响闭包的类型,闭包实现哪种Trait依然仅仅取决于闭包内部如何使用变量。比如下面这段代码能够编译通过.
fn test(_t:impl FnOnce()){ } fn main() { let s = String::from("abc"); let f = move || println!("{}",s); test(f); } //
这有一个奇妙的例子:
fn main(){ let mut a = 1; let mut inc = || {a+=1;a}; println!("now a is {}", a); inc(); }
let mut inc = || {a+=1;a}; | -- - first borrow occurs due to use of `a` in closure | | | mutable borrow occurs here 4 | println!("now a is {}", a); | ^ immutable borrow occurs here 5 | inc(); | --- mutable borrow later used here
很明显闭包在在初始化的时候就已经发生了借用行为。为啥会这样呢,不是说闭包在初始化的时候不执行吗?原因是闭包本身是个匿名结构体,初始化的时候会根据闭包体内的代码把外部变量按照相应的方式捕获进结构体,这里显然是mutable borrow方式;而实际执行给a+1的时间节点在闭包执行的时候,执行的本质就是匿名结构体操作自己的成员变量。所以借用是发生在初始化的,所以报了这个错误。
fn main(){ let mut s = String::from("test"); let mut f = || {s.push('a');println!("{}", s)}; f(); f(); }
这种闭包为什么能多次执行,是因为s的所有权直接被拿到匿名结构体里面去了,闭包每次实际执行push都是在匿名结构体里面的s上执行。这也是为啥当闭包里面有修改操作的时候闭包本身必须用mut修饰,因为执行修改操作的时候闭包本身被改了,它就必须是mut的了。
move关键字:
这个东西不可与FnOnce混为一谈。某些情况下,我们需要闭包本身(匿名结构体本身)获取变量的所有权,即使闭包体内的代码并不需要变量的所有权(考虑下在线程间传递闭包),这时需要使用move关键字强制Rust将所有捕获的变量移动给闭包本身,以延长被移动的对象的生存期。
-
接收闭包作为参数&&返回闭包 ( 接收闭包比返回闭包多了一个Trait Bounds的方式
由于impl方式不涉及到泛型,所以有些泛型限定不能用。更多时候接收闭包的代码更倾向于使用Trait Bounds的方式。
-
接收闭包
可以用Rust书上写的那种Box dyn来接收,但是有运行时惩罚,也可以用泛型+trait bounds接收,也可以用impl Trait的方式接收.
dyn方式:
fn fuck(f:Box<dyn Fn(i32)->i32>,arg:i32){ println!("{}",f(arg)) } fn main(){ let f=|num:i32|num+1; fuck(Box::new(f), 1); }
也可以用 泛型+Trait Bounds:
fn fuck<F>(f:F,arg:i32) where F:Fn(i32)->i32 { println!("{}",f(arg)) } fn main(){ let f=|num|num+1; fuck(f, 1); }
impl trait 的方式:
fn fuck(f:impl Fn(i32)->i32,arg:i32){ println!("{}",f(arg)) } fn main(){ let f=|num|num+1; fuck(f, 1); }
-
返回闭包
在需要返回Trait的时候, 一般情况下可以返回实现了Trait的具体类型,但是,闭包只能表现为Trait且Trait是DST的一种,所以不能直接返回闭包:它没有一个具体的类型,它能且只能表现为Triat。实现某Trait的类型们在堆上所需的内存大小是不一样的,但是一个指向它们的指针的大小是确定的,所以可以用Box + dyn 来完成这个任务。
之所以写dyn是为了清晰语义,否则Trait Object和Trait写起来太像了。
fn main() { let f = return_a_closure(); let t = f(1); assert_eq!(t, 2); } fn return_a_closure() -> Box<dyn Fn(i32) -> i32> { Box::new(|x: i32| x + 1) }
上面这段内容是Rust官方教程里的,但是现在有除了trait object之外的方式来返回closures了。https://doc.rust-lang.org/reference/types/impl-trait.html#abstract-return-types
下面的内容来自Rust by Example:
**// 这个函数返回了一个闭包,但是返回值这里的类型限定它不光是写了实现了Fn这个Trait, // 它还详细写了这个闭包接收的参数类型以及返回的参数类型** fn returns_closure() -> impl Fn(i32) -> i32 { |x| x + 1 } fn main(){ println!("{}",returns_closure()(1)) }
impl trait语法来自https://doc.rust-lang.org/std/keyword.impl.html 最后一段:
The other use of theimpl
keyword is inimpl
Trait syntax, which can be seen as a shorthand for “a concrete type that implements this trait”. Its primary use is working with closures, which have type definitions generated at compile time that can’t be simply typed out.rust by example解释说:impl trait provides ways to specify unnamed but concrete types that implement a specific trait. It can appear in two sorts of places: argument position (where it can act as an anonymous type parameter to functions), and return position (where it can act as an abstract return type).
fn thing_returning_closure() -> impl Fn(i32) -> bool { println!("here's a closure for you!"); |x: i32| x % 3 == 0 }
a concrete type that implements this trait” 可以解释为什么要把闭包的参数列表和返回值列表都写清楚,因为
impl
关键字指的是 实现这个Trait的concerte type。既然是concerte type,那就要把各种限定都写明白。也可以理解为,闭包的类型是独一无二的,无法书写出来的类型,只能通过Fn trait去写。
impl Trait
可以比Trait object
写得更简单。且由于Trait object
中包裹了Box
,导致有运行时惩罚。
-
-
完整的例子
fn receive1<T>(f: T) where T: Fn(i32) -> i32, { let num = 4; let num = f(num); println!("{}", num); } fn receive2(f: impl Fn(i32) -> i32) { let num = 10; let num = f(num); println!("{}", num); } fn receive3(f: Box<dyn Fn(i32) -> i32>) { let num = 15; let num = f(num); println!("{}", num); } fn return1() -> impl FnOnce(String) -> String { let greeting = String::from("hello budy, "); move |name: String| format!("{} {}", greeting, name) } fn return2() -> Box<dyn FnOnce(String) -> String> { let greeting = String::from("hello budy, "); Box::new(move |name: String| format!("{} {}", greeting, name)) } fn main() { let t=|a| a+1; receive1(t); receive2(t); receive3(Box::new(t)); println!(" {} ",return1()(String::from("jhon"))); println!(" {} ",return2()(String::from("nerd"))) }
receive123分别是 trait bounds ,impl trait , trait object
return12分别是 impl trait , trait object