rust閉包

字號+ 編輯: 国内TP粉 修訂: SyncLWT 來源: 张汉东 2023-09-10 我要說兩句(0)

來自張漢東rust編程之道

概述

我們常常需要回調函數的功能, 需要函數並不是在創建時執行, 而是以回調的方式, 在需要的時候延遲執行. 並且, 常常需要在函數中獲取環境中的一些信息, 又不需要將其作爲函數參數傳入. 這種應用場景就需要閉包這一工具了.

閉包是持有外部環境變量的函數. 所謂外部環境, 就是指創建閉包時所在的詞法作用域.

閉包的語法:

|params| {expr}

其中params表示向閉包中傳遞的參數, 類似於函數參數. 可以顯式指定類型, 也可由編譯器自動推導.

expr表示閉包中的各種表達式, 其返回值類型作爲爲閉包的返回值類型.

let a = "hello";
let print = || {println!("{:?}", a);};
print();

上面的代碼段創建了一個閉包, 列印環境變量a的值, 沒有傳入參數, 返回值類型爲().

分類

使用環境變量的方式

Rust中的閉包, 按照對捕獲變量的使用方式, 將閉包分爲三個類型: Fn, FnMut, FnOnce. 其中Fn類型的閉包, 在閉包内部以共享借用的方式使用環境變量; FnMut類型的閉包, 在閉包内部以獨佔借用的方式使用環境變量; 而FnOnce類型的閉包, 在閉包内部以所有者的身份使用環境變量. 由此可見, 根據閉包内使用環境變量的方式, 即可判斷創建出來的閉包的類型.

注意, 對於Copy類型的環境變量, 如果以傳值的方式使用, 其默認的閉包類型是Fn, 而非FnOnce, 而對非Copy的環境變量, 其閉包類型只能是FnOnce.

閉包中環境變量最終的捕獲方式 (即, 是借用, 是複制, 還是轉移所有權), 還與環境變量本身的語義, 以及閉包是否強制獲取環境變量的所有權有關.

舉例說明:

#![feature(fn_traits)]
fn main() {
    let mut a = 1;
    let mut print = || {  &a; };
    print.call_once(()); // OK
    print.call_mut(()); // OK
    print.call(()); // OK
}
#![feature(fn_traits)]
fn main() {
    let mut a = 1;
    let mut print = || {  &mut a; };
    print.call_once(()); // OK
    print.call_mut(()); // OK
    print.call(()); // error, the requirement to implement `Fn` derives from here
}
#![feature(fn_traits)]
fn main() {
    let mut a = 1;
    let mut print = || {  a; };
    print.call_once(()); // OK
    print.call_mut(()); // OK
    print.call(()); // OK
}

最後這個比較神奇, 印象中以爲Copy和非Copy的環境變量, 而實際上創建的閉包由於環境變量都是Copy的, 默認實現了Fn. 如果是非Copy的環境變量, 則只能實現FnOnce.

#![feature(fn_traits)]
fn main() {
    let mut a = "str".to_string();
    let mut print = || {  a; };

    print.call_once(()); // OK
    print.call_mut(()); // error, the requirement to implement `FnMut` derives from here
    print.call(()); // error, the requirement to implement `Fn` derives from here
}

是否強制move

在閉包的管道符前面加上move關鍵字, 會強制以傳值的方式捕獲變量. 至於是複制還是移動, 則與環境變量類型的語義有關. 我們知道, 一個類型實現Copy, 即爲複制語義. 在作爲右值使用時會將值按位複制. 而未實現Copy的類型即爲移動語義, 作右值使用時會轉移所有權.

舉個例子:

// 沒有強制move, 不強制按值捕獲變量

fn main() {

    let mut a = 1;

    let print = || { &a; };

    let aa = &mut a; // 這裡編譯報錯, 發生了非法的可變借用 mutable borrow occurs here


    print();

}

之所以聲明可變借用aa編譯報錯, 是因爲創建閉包時, 由於是使用可變借用, 因此默認按可變借用捕獲環境變量a. 我們知道, 可變借用和不可變借用不能同時使用.

// 強制move, 按值捕獲變量
fn main() {
    let mut a = 1;
    let print = move || {
        // 這裡添加move, 強制按值捕獲變量
        &a;
    };
    let aa = &mut a; // 這裡不報錯, 因爲閉包中複制了a的值 print();
}

環境變量的語義

雖然環境變量的類型的語義不影響捕獲方式, 但卻會影響創建出來的閉包的性質. 如果所有捕獲的環境變量均爲Copy, 則閉包爲Copy, 否則閉包爲非Copy, 需要移動.

舉個例子:

// 環境變量是Copy, 則閉包是Copy
fn main() {
    let mut a = 1;
    let print = move || { a; };
    let print2 = print; // 因爲閉包只捕獲了a, 而a是i32是Copy的, 所以print是Copy的 print(); // 這裡沒有發生所有權轉移, 是按位複制, print仍然可用 print2();
}
// 環境變量非Copy, 則閉包非Copy
fn main() {
    let mut a = 1;
    let mut s = "str".to_string(); let print = move || { a; s; };
    let print2 = print;

    print(); // 這裡就要報錯了, value used here after move print2();
}

用法

61727b8f5bbdeed2fbd29edfae7ce10c.jpeg

閉包的用法在張漢東《Rust編程之道》這本書中有比較詳細的說明, 主要有兩種用法, 作爲函數參數, 作爲函數返回值. 其中, 作爲函數返回值時, 需要注意FnOnce需要特殊處理, Rust會將其封裝成FnBox, 從而解決閉包trait對象在解引用時的拆箱問題.

其他

閉包的逃逸性

根據一個閉包是否會逃逸到創建該閉包的詞法作用域之外, 可以將閉包分爲非逃逸閉包逃逸閉包.

這二者最根本的區別在於, 逃逸閉包必須複制或移動環境變量. 這是很顯然的, 如果閉包在詞法作用域之外使用, 而其如果以引用的方式獲取環境變量, 有可能引起懸垂指針問題.

逃逸閉包的類型聲明中, 需要加一個靜態生命周期參數'static.

// 非逃逸閉包, 不按值捕獲環境變量也可以編譯通過
fn main() {
    let a = 1;
    let c: Box<Fn()> = Box::new(
        || { &a; }
    );
}
// 顯式聲明類型爲逃逸閉包, 不按值捕獲環境變量會編譯失敗
fn main() {
    let a = 1;
    let c: Box<Fn()+'static> = Box::new(
        || {
            &a; // error, borrowed value does not live long enough
        }
    );
}
// 顯式聲明類型爲逃逸閉包, 按值捕獲環境變量, 編譯通過
fn main() {
    let a = 1;
    let c: Box<Fn()+'static> = Box::new(
        move || { &a; }
    );
}

高階生命周期

主要解決閉包參數中含有引用時的生命周期標注的問題. Rust通過高階trait限定的for<>語法, 解決這一問題.

總結

閉包的幾個關鍵點:

  1. 閉包如何捕獲環境變量: 與環境變量是否Copy, 是否強制move有關.

  2. 閉包類型: 與環境變量是否Copy, 環境變量在閉包中的使用方式有關.

  3. 閉包在何時使用環境變量: 涉及閉包的逃逸性, 逃逸閉包必須傳值.


閲完此文,您的感想如何?
  • 有用

    0

  • 沒用

    0

  • 開心

    0

  • 憤怒

    0

  • 可憐

    0

1.如文章侵犯了您的版權,請發郵件通知本站,該文章將在24小時内刪除;
2.本站標注原創的文章,轉發時煩請注明來源;
3.交流群: 2702237 13835667

相關課文
  • 在rust/axum框架中操作redis

  • rust編譯新的wasm項目操作流程(原文: 編譯 Rust 爲 WebAssembly)

  • rust視圖模板庫askama的使用

  • axum框架當中獲取請求header, 和獲取header指定字段的方法

我要說說
網上賓友點評