Fn FnMut FnOnce 傻傻分不清
上週文享了閉包你瞭解底層實現嘛? 我們要記住,閉包是由函數和與其相關的引用環境組合而成的實體
同時閉包引用變量也是有優先級的:優先只讀借用,然後可變借用,最後轉移所有權。本篇文章看下,如何將閉包當成參數或返回值
Go 閉包調用
package main
import "fmt"
func test(f func()) {
f()
f()
}
func main() {
a:=1
fn := func() {
a++
fmt.Printf("a is %d\n", a)
}
test(fn)
}
上面是 go 的閉包調用,我們把 fn
當成參數,傳給函數 test
. 閉包捕獲變量 a, 做自增操作,同時函數 fn
可以調用多次
對於熟悉 go 的人來說,這是非常自然的,但是換成 rust 就有問題了
fn main() {
let s = String::from("wocao");
let f = || {println!("{}", s);};
f();
}
比如上面這段 rust 代碼,我如果想把閉包 f
當成參數該怎麼寫呢?上週分享的閉包我們知道,閉包是匿名的
c = hello_cargo::main::closure-2 (0x7fffffffe0e0, 0x7fffffffe0e4)
b = hello_cargo::main::closure-1 (0x7fffffffe0e0)
a = hello_cargo::main::closure-0
在運行時,類似於上面的結構體,閉包結構體命名規則 closure-xxx
, 同時我們是不知道函數簽名的
引出 Trait
官方文檔 給出了方案,標準庫提供了幾個內置的 trait
, 一個閉包一定實現了 Fn
, FnMut
, FnOnce
其中一個,然後我們可以用泛型 + trait 的方式調用閉包
$ cat src/main.rs
fn test<T>(f: T) where
T: Fn()
{
f();
}
fn main() {
let s = String::from("董澤潤的技術筆記");
let f = || {println!("{}", s);};
test(f);
}
$ cargo run
Finished dev [unoptimized + debuginfo] target(s) in 0.00s
Running `target/debug/hello_cargo`
董澤潤的技術筆記
上面將閉包 f
以泛型參數的形式傳給了函數 test
, 因爲閉包實現了 Fn
trait. 剛學這塊的人可能會糊塗,其實可以理解類比 go interface
, 但本質還是不一樣的
let f = || {s.push_str("不錯");};
假如 test
聲明不變,我們的閉包修改了捕獲的變量呢?
|
9 | let f = || {s.push_str("不錯");};
| ^^ - closure is `FnMut` because it mutates the variable `s` here
| |
| this closure implements `FnMut`, not `Fn`
10 | test(f);
報錯說 closure 實現的 trait 是 FnMut
, 而不是 Fn
fn test<T>(mut f: T) where
T: FnMut()
{
f();
}
fn main() {
let mut s = String::from("董澤潤的技術筆記");
let f = || {s.push_str("不錯");};
test(f);
}
上面是可變借用的場景,我們再看一下 move 所有權的情況
fn test<T>(f: T) where
T: FnOnce()
{
f();
}
fn main() {
let s = String::from("董澤潤的技術筆記");
let f = || {let _ = s;};
test(f);
}
上面我們把自由變量 s 的所有權 move 到了閉包裏,此時 T
泛型的特徵變成了 FnOnce
, 表示只能執行一次。那如果 test
調用閉包兩次呢?
1 | fn test<T>(f: T) where
| - move occurs because `f` has type `T`, which does not implement the `Copy` trait
...
4 | f();
| --- `f` moved due to this call
5 | f();
| ^ value used here after move
|
note: this value implements `FnOnce`, which causes it to be moved when called
--> src/main.rs:4:5
|
4 | f();
編譯器提示第一次調用的時候,己經 move 了,再次調用無法訪問。很明顯此時自由變量己經被析構了 let _ = s;
離開詞法作用域就釋放了,rust 爲了內存安全當然不允許繼續訪問
fn test<T>(f: T) where
T: Fn()
{
f();
f();
}
fn main() {
let s = String::from("董澤潤的技術筆記");
let f = move || {println!("s is {}", s);};
test(f);
//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
代表轉移了所有權,同時只能執行一次,再執行的話自由變量脫離作用域回收了
# mod foo {
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
, FnOnce
的實現。可以看到 Fn
繼承自 FnMut
, FnMut
繼承自 FnOnce
Fn(u32) -> u32
前文例子都是無參數的,其實還可以帶上參數
由於 Fn
是繼承自 FnMut
, 那麼我們把實現 Fn
的閉包傳給 FnMut
的泛型可以嘛?
$ cat src/main.rs
fn test<T>(mut f: T) where
T: FnMut()
{
f();
f();
}
fn main() {
let s = String::from("董澤潤的技術筆記");
let f = || {println!("s is {}", s);};
test(f);
}
$ cargo run
Compiling hello_cargo v0.1.0 (/Users/zerun.dong/code/rusttest/hello_cargo)
Finished dev [unoptimized + debuginfo] target(s) in 1.47s
Running `target/debug/hello_cargo`
s is 董澤潤的技術筆記
s is 董澤潤的技術筆記
當然可以看起來沒有問題,FnMut
告訴函數 test
這是一個會修改變量的閉包,那麼傳進來的閉包不修改當然也沒問題
上圖比較出名,由於有繼承關係,實現 Fn
可用於 FnMut
和 FnOnce
參數,實現 FnMut
可用於 FnOnce
參數
函數指針
fn call(f: fn()) { // function pointer
f();
}
fn main() {
let a = 1;
let f = || println!("abc"); // anonymous function
let c = || println!("{}", &a); // closure
call(f);
call(c);
}
函數和閉包是不同的,上面的例子中 f
是一個匿名函數,而 c
引用了自由變量,所以是閉包。這段代碼是不能執行的
9 | let c = || println!("{}", &a); // closure
| --------------------- the found closure
...
12 | call(c);
| ^ expected fn pointer, found closure
編譯器告訴我們,12 行要求參數是函數指針,不應該是閉包
閉包作爲返回值
參考 impl Trait 輕鬆返回複雜的類型,impl Trait
是指定實現特定特徵的未命名但有具體類型的新方法。你可以把它放在兩個地方:參數位置和返回位置
fn returns_closure() -> Box<dyn Fn(i32) -> i32> {
Box::new(|x| x + 1)
}
fn main() {
let f = returns_closure();
println!("res is {}", f(11));
}
在以前,從函數處返回閉包的唯一方法是,使用 trait
對象,大家可以試試不用 Box
裝箱的報錯提示
fn returns_closure() -> impl Fn(i32) -> i32 {
|x| x + 1
}
fn main() {
let f = returns_closure();
println!("res is {}", f(11));
}
現在我們可以用 impl 來實現閉包的返回值聲明
fn test() -> impl FnMut(char) {
let mut s = String::from("董澤潤的技術筆記");
|c| { s.push(c); }
}
fn main() {
let mut c = test();
c('d');
c('e');
}
來看一個和引用生命週期相關的例子,上面的代碼返回閉包 c
, 對字符串 s 進行追回作。代碼執行肯定報錯:
--> src/main.rs:3:5
|
3 | |c| { s.push(c); }
| ^^^ - `s` is borrowed here
| |
| may outlive borrowed value `s`
|
note: closure is returned here
--> src/main.rs:1:14
|
1 | fn test() -> impl FnMut(char) {
| ^^^^^^^^^^^^^^^^
help: to force the closure to take ownership of `s` (and any other referenced variables), use the `move` keyword
|
3 | move |c| { s.push(c); }
| ^^^^^^^^
提示的很明顯,變量 s
脫離作用域就釋放了,編譯器也提示我們要 move 所有權給閉包,感興趣的自己修改測試一下
小結
分享知識,長期輸出價值,這是我做公衆號的目標。同時寫文章不容易,如果對大家有所幫助和啓發,請幫忙點擊在看
,點贊
,分享
三連
關於 閉包
大家有什麼看法,歡迎留言一起討論,大牛多留言 ^_^
本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源:https://mp.weixin.qq.com/s/sxfkVBjGSc6J1sVIWVdvTw