Fn FnMut FnOnce 傻傻分不清

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

[[415256]]

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

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

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<T>(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<T>(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<T>(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<T>(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<T>(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 选择在编译期做检查

  • FnOnce consumes the variables it captures from its enclosing scope, known as the closure’s environment. To consume the captured variables, the closure must take ownership of these variables and move them into the closure when it is defined. The Once part of the name represents the fact that the closure can’t take ownership of the same variables more than once, so it can be called only once.
  • FnMut can change the environment because it mutably borrows values.
  • Fn borrows values from the environment immutably.

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

  1. # mod foo { 
  2. pub trait Fn<Args> : FnMut<Args> { 
  3.     extern "rust-call" fn call(&self, args: Args) -> Self::Output
  4.  
  5. pub trait FnMut<Args> : FnOnce<Args> { 
  6.     extern "rust-call" fn call_mut(&mut self, args: Args) -> Self::Output
  7.  
  8. pub trait FnOnce<Args> { 
  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<T>(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. is 董泽润的技术笔记 
  6. 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<dyn Fn(i32) -> 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 所有权给闭包,感兴趣的自己修改测试一下

 

责任编辑:武晓燕 来源: 董泽润的技术笔记
相关推荐

2022-05-05 13:54:37

SPI机制APISPI

2021-03-23 10:45:23

CookieSession前端

2021-01-22 08:37:02

二进制Binary SemaReentrant L

2021-10-06 23:31:45

HibernateJPASpring Data

2021-05-09 21:57:08

数据仓库Olap

2021-03-10 08:56:37

Zookeeper

2021-07-27 07:31:16

JavaArrayList数组

2022-05-15 21:52:04

typeTypeScriptinterface

2024-02-29 09:08:56

Encoding算法加密

2021-08-02 12:50:45

sessiontokenJava

2019-06-18 09:45:19

正向代理反向代理IT

2020-10-30 08:20:04

SD卡TF卡存储

2022-04-15 11:47:47

LVSNginx负载均衡

2018-12-17 12:30:05

Kubernetes存储存储卷

2018-05-22 16:24:20

HashMapJavaJDK

2015-02-28 10:01:00

云计算IT架构虚拟化

2020-03-03 17:35:09

Full GCMinor

2023-02-27 15:46:19

数据元元数据

2023-09-03 21:18:07

Python编程语言

2021-02-25 23:40:26

Redis集群架构
点赞
收藏

51CTO技术栈公众号