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 調用閉包兩次呢?

| 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
  ||     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 選擇在編譯期做檢查

上面來自官網的解釋,****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 可用於 FnMutFnOnce 參數,實現 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
  ||     |c| { s.push(c); }
  |     ^^^   - `s` is borrowed here
  |     |
  |     may outlive borrowed value `s`
  |
note: closure is returned here
 --> src/main.rs:1:14
  || fn test() -> impl FnMut(char) {
  |              ^^^^^^^^^^^^^^^^
help: to force the closure to take ownership of `s` (and any other referenced variables), use the `move` keyword
  ||     move |c| { s.push(c); }
  |     ^^^^^^^^

提示的很明顯,變量 s 脫離作用域就釋放了,編譯器也提示我們要 move 所有權給閉包,感興趣的自己修改測試一下

小結

分享知識,長期輸出價值,這是我做公衆號的目標。同時寫文章不容易,如果對大家有所幫助和啓發,請幫忙點擊在看點贊分享 三連

關於 閉包 大家有什麼看法,歡迎留言一起討論,大牛多留言 ^_^

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/sxfkVBjGSc6J1sVIWVdvTw