1. 程式人生 > 實用技巧 >Rust點滴: 閉包那點事兒

Rust點滴: 閉包那點事兒

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();
}
複製程式碼

用法

閉包的用法在<<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<>語法, 解決這一問題.

總結

閉包的幾個關鍵點:

  • 閉包如何捕獲環境變數: 與環境變數是否Copy, 是否強制move有關.
  • 閉包型別: 與環境變數是否Copy, 環境變數在閉包中的使用方式有關.
  • 閉包在何時使用環境變數: 涉及閉包的逃逸性, 逃逸閉包必須傳值.

參考資料