13518219792

建站动态

根据您的个性需求进行定制 先人一步 抢占小程序红利时代

FnFnMutFnOnce傻傻分不清

本文转载自微信公众号「董泽润的技术笔记」,作者董泽润。转载本文请联系董泽润的技术笔记公众号。

创新互联专注于富民网站建设服务及定制,我们拥有丰富的企业做网站经验。 热诚为您提供富民营销型网站建设,富民网站制作、富民网页设计、富民网站官网定制、小程序开发服务,打造富民网络公司原创品牌,更为您提供富民网站排名全网营销落地服务。

同时闭包引用变量也是有优先级的:优先只读借用,然后可变借用,最后转移所有权。本篇文章看下,如何将闭包当成参数或返回值

Go 闭包调用

  
 
 
 
  1. package main 
  2.  
  3. import "fmt" 
  4.  
  5. func test(f func()) { 
  6.     f() 
  7.     f() 
  8.  
  9. func main() { 
  10.     a:=1 
  11.     fn := func() { 
  12.         a++ 
  13.         fmt.Printf("a is %d\n", a) 
  14.     } 
  15.     test(fn) 

上面是 go 的闭包调用,我们把 fn 当成参数,传给函数 test. 闭包捕获变量 a, 做自增操作,同时函数 fn 可以调用多次

对于熟悉 go 的人来说,这是非常自然的,但是换成 rust 就有问题了

  
 
 
 
  1. fn main() { 
  2.     let s = String::from("wocao"); 
  3.     let f = || {println!("{}", s);}; 
  4.     f(); 

比如上面这段 rust 代码,我如果想把闭包 f 当成参数该怎么写呢?上周分享的闭包我们知道,闭包是匿名的

  
 
 
 
  1. c = hello_cargo::main::closure-2 (0x7fffffffe0e0, 0x7fffffffe0e4) 
  2. b = hello_cargo::main::closure-1 (0x7fffffffe0e0) 
  3. a = hello_cargo::main::closure-0 

在运行时,类似于上面的结构体,闭包结构体命名规则 closure-xxx, 同时我们是不知道函数签名的

引出 Trait

官方文档 给出了方案,标准库提供了几个内置的 trait, 一个闭包一定实现了 Fn, FnMut, FnOnce 其中一个,然后我们可以用泛型 + trait 的方式调用闭包

  
 
 
 
  1. $ cat src/main.rs 
  2. fn test(f: T) where 
  3.     T: Fn() 
  4.     f(); 
  5.  
  6. fn main() { 
  7.     let s = String::from("董泽润的技术笔记"); 
  8.     let f = || {println!("{}", s);}; 
  9.     test(f); 
  10.  
  11. $ cargo run 
  12.     Finished dev [unoptimized + debuginfo] target(s) in 0.00s 
  13.      Running `target/debug/hello_cargo` 
  14. 董泽润的技术笔记 

上面将闭包 f 以泛型参数的形式传给了函数 test, 因为闭包实现了 Fn trait. 刚学这块的人可能会糊涂,其实可以理解类比 go interface, 但本质还是不一样的

  
 
 
 
  1. let f = || {s.push_str("不错");}; 

假如 test 声明不变,我们的闭包修改了捕获的变量呢?

  
 
 
 
  1. |     let f = || {s.push_str("不错");}; 
  2. |             ^^  - closure is `FnMut` because it mutates the variable `s` here 
  3. |             | 
  4. |             this closure implements `FnMut`, not `Fn` 
  5. |     test(f); 

报错说 closure 实现的 trait 是 FnMut, 而不是 Fn

  
 
 
 
  1. fn test(mut f: T) where 
  2.     T: FnMut() 
  3.     f(); 
  4.  
  5. fn main() { 
  6.     let mut s = String::from("董泽润的技术笔记"); 
  7.     let f = || {s.push_str("不错");}; 
  8.     test(f); 

上面是可变借用的场景,我们再看一下 move 所有权的情况

  
 
 
 
  1. fn test(f: T) where 
  2.     T: FnOnce() 
  3.     f(); 
  4.  
  5. fn main() { 
  6.     let s = String::from("董泽润的技术笔记"); 
  7.     let f = || {let _ = s;}; 
  8.     test(f); 

上面我们把自由变量 s 的所有权 move 到了闭包里,此时 T 泛型的特征变成了 FnOnce, 表示只能执行一次。那如果 test 调用闭包两次呢?

  
 
 
 
  1. 1 | fn test(f: T) where 
  2.   |            - move occurs because `f` has type `T`, which does not implement the `Copy` trait 
  3. ... 
  4. 4 |     f(); 
  5.   |     --- `f` moved due to this call 
  6. 5 |     f(); 
  7.   |     ^ value used here after move 
  8.   | 
  9. note: this value implements `FnOnce`, which causes it to be moved when called 
  10.  --> src/main.rs:4:5 
  11.   | 
  12. 4 |     f(); 

编译器提示第一次调用的时候,己经 move 了,再次调用无法访问。很明显此时自由变量己经被析构了 let _ = s; 离开词法作用域就释放了,rust 为了内存安全当然不允许继续访问

  
 
 
 
  1. fn test(f: T) where 
  2.     T: Fn() 
  3.     f(); 
  4.     f(); 
  5.  
  6. fn main() { 
  7.     let s = String::from("董泽润的技术笔记"); 
  8.     let f = move || {println!("s is {}", s);}; 
  9.     test(f); 
  10.     //println!("{}", s); 

那么上面的代码例子, 是否可以运行呢?当然啦,此时变量 s 的所有权 move 给了闭包 f, 生命周期同闭包,反复调用也没有副作用

深入理解

本质上 Rust 为了内存安全,才引入这么麻烦的处理。平时写 go 程序,谁会在乎对象是何时释放,对象是否存在读写冲突呢?总得有人来做这个事情,Rust 选择在编译期做检查

上面来自官网的解释,Fn 代表不可变借用的闭包,可重复执行,FnMut 代表闭包可变引用修改了变量,可重复执行 FnOnce 代表转移了所有权,同时只能执行一次,再执行的话自由变量脱离作用域回收了

  
 
 
 
  1. # mod foo { 
  2. pub trait Fn : FnMut { 
  3.     extern "rust-call" fn call(&self, args: Args) -> Self::Output; 
  4.  
  5. pub trait FnMut : FnOnce { 
  6.     extern "rust-call" fn call_mut(&mut self, args: Args) -> Self::Output; 
  7.  
  8. pub trait FnOnce { 
  9.     type Output; 
  10.  
  11.     extern "rust-call" fn call_once(self, args: Args) -> Self::Output; 
  12. # } 

上面是标准库中,Fn, FnMut, FnOnce 的实现。可以看到 Fn 继承自 FnMut, FnMut 继承自 FnOnce

  
 
 
 
  1. Fn(u32) -> u32 

前文例子都是无参数的,其实还可以带上参数

由于 Fn 是继承自 FnMut, 那么我们把实现 Fn 的闭包传给 FnMut 的泛型可以嘛?

  
 
 
 
  1. $ cat src/main.rs 
  2. fn test(mut f: T) where 
  3.     T: FnMut() 
  4.     f(); 
  5.     f(); 
  6.  
  7. fn main() { 
  8.     let s = String::from("董泽润的技术笔记"); 
  9.     let f = || {println!("s is {}", s);}; 
  10.     test(f); 
  
 
 
 
  1. $ cargo run 
  2.    Compiling hello_cargo v0.1.0 (/Users/zerun.dong/code/rusttest/hello_cargo) 
  3.     Finished dev [unoptimized + debuginfo] target(s) in 1.47s 
  4.      Running `target/debug/hello_cargo` 
  5. s is 董泽润的技术笔记 
  6. s is 董泽润的技术笔记 

当然可以看起来没有问题,FnMut 告诉函数 test 这是一个会修改变量的闭包,那么传进来的闭包不修改当然也没问题

上图比较出名,由于有继承关系,实现 Fn 可用于 FnMut 和 FnOnce 参数,实现 FnMut 可用于 FnOnce 参数

函数指针

  
 
 
 
  1. fn call(f: fn()) {    // function pointer 
  2.     f(); 
  3.  
  4. fn main() { 
  5.     let a = 1; 
  6.  
  7.     let f = || println!("abc");     // anonymous function 
  8.     let c = || println!("{}", &a);  // closure 
  9.  
  10.     call(f); 
  11.     call(c); 

函数和闭包是不同的,上面的例子中 f 是一个匿名函数,而 c 引用了自由变量,所以是闭包。这段代码是不能执行的

  
 
 
 
  1. 9  |     let c = || println!("{}", &a);  // closure 
  2.    |             --------------------- the found closure 
  3. ... 
  4. 12 |     call(c); 
  5.    |          ^ expected fn pointer, found closure 

编译器告诉我们,12 行要求参数是函数指针,不应该是闭包

闭包作为返回值

参考 impl Trait 轻松返回复杂的类型,impl Trait 是指定实现特定特征的未命名但有具体类型的新方法。你可以把它放在两个地方:参数位置和返回位置

  
 
 
 
  1. fn returns_closure() -> Box i32> { 
  2.     Box::new(|x| x + 1) 
  3.  
  4. fn main() { 
  5.     let f = returns_closure(); 
  6.     println!("res is {}", f(11)); 

在以前,从函数处返回闭包的唯一方法是,使用 trait 对象,大家可以试试不用 Box 装箱的报错提示

  
 
 
 
  1. fn returns_closure() -> impl Fn(i32) -> i32 { 
  2.     |x| x + 1 
  3.  
  4. fn main() { 
  5.     let f = returns_closure(); 
  6.     println!("res is {}", f(11)); 

现在我们可以用 impl 来实现闭包的返回值声明

  
 
 
 
  1. fn test() -> impl FnMut(char) { 
  2.     let mut s = String::from("董泽润的技术笔记"); 
  3.     |c| { s.push(c); } 
  4.  
  5. fn main() { 
  6.     let mut c = test(); 
  7.     c('d'); 
  8.     c('e'); 

来看一个和引用生命周期相关的例子,上面的代码返回闭包 c, 对字符串 s 进行追回作。代码执行肯定报错:

  
 
 
 
  1.  --> src/main.rs:3:5 
  2.   | 
  3. 3 |     |c| { s.push(c); } 
  4.   |     ^^^   - `s` is borrowed here 
  5.   |     | 
  6.   |     may outlive borrowed value `s` 
  7.   | 
  8. note: closure is returned here 
  9.  --> src/main.rs:1:14 
  10.   | 
  11. 1 | fn test() -> impl FnMut(char) { 
  12.   |              ^^^^^^^^^^^^^^^^ 
  13. help: to force the closure to take ownership of `s` (and any other referenced variables), use the `move` keyword 
  14.   | 
  15. 3 |     move |c| { s.push(c); } 
  16.   |     ^^^^^^^^ 

提示的很明显,变量 s 脱离作用域就释放了,编译器也提示我们要 move 所有权给闭包,感兴趣的自己修改测试一下


分享题目:FnFnMutFnOnce傻傻分不清
本文URL:http://cdbrznjsb.com/article/coiphhs.html

其他资讯

让你的专属顾问为你服务