Skip to content

Instantly share code, notes, and snippets.

@KmolYuan
Last active August 18, 2022 15:27
Show Gist options
  • Save KmolYuan/3e15e9531bfe9992b32f6ceea807a430 to your computer and use it in GitHub Desktop.
Save KmolYuan/3e15e9531bfe9992b32f6ceea807a430 to your computer and use it in GitHub Desktop.

Rust 閉包 Closure 解說

閉包又稱匿名函式 (Anonymous function)、Lambda 函式等,對程式語言來說是個抽象 (abstract) 的語意。與普通的靜態函式 (Static function) 相比,多出了「捕捉 (capture)」的功能,可以將自身以外的變數拿來引用。

靜態函式僅允許變數以「參數 (argument)」的方式輸入,當有大量參數必須輸入時,可以建立一個結構體 (structure) 類型來包裝相關的變數。閉包的原理亦是如此,使用一個暫時的結構體將變數存入,再將其他參數傳入,執行其專用函式。

let a = 10;
let closure = |x: i32| x * a;
closure(10);

// equivalent to
let a = 10;
let closure = {
    struct Closure<'a> {
        a: &'a i32,
    }
    impl Closure<'_> {
        fn call(&self, x: i32) -> i32 {
            x * self.a
        }
    }
    Closure { a: &a }
};
closure.call(10);

捕捉 Capture

只要是函式內沒有宣告、也非參數的變數被引用,就會啟動「捕捉」。Rust 針對這些變數是十分嚴格的,如下表所示:

名稱 閉包內語法 產生 lifetime 可變
唯讀引用 &a, calling &self method yes no
可變引用 &mut a, calling &mut self method yes yes
移動 / 複製 a, must use move leading keyword no no

上表可見,使用參照 (reference) 會使這個閉包成為帶有一個 'a liftime 的泛型變數,'a 與生命週期最長的變數相同,意即此閉包必須在該變數結束前摧毀。如果都沒有參照,則為一個靜態 'static 閉包,如果移動 / 複製的變數允許 Send / Sync,或沒用到 move,此閉包可在執行緒 (thread) 之間發送。

  • 注意!參照物件 a: &A 本質上是指標 (pointer),也需要用 move 複製才可使用。
  • 注意!可變參照物件 a: &mut A 無法複製,move 會使用移動。

以下為三種分別使用上述不同方法的閉包,其中可變引用的閉包在呼叫時一定要標示 mut 成為可變變數。注意!這三種語法是可以混用的。

let mut a = 10;
let readonly = || {
  &a;
};
let mut mutable = || {
  &mut a;
};
let movable = move || {
  a;
};

泛型邊界 Generic Bounds

前面已提過,閉包其實為一種專用的結構體搭配專用函式,所以沒有固定的類型名稱(會以亂數產生)。靜態閉包可以像靜態函式有 fn() -> bool 這種已知類型的函式指標 (function pointer),但是其餘除外。在 Rust 中,只能以泛型描述非靜態的閉包,有下列三種:

  • trait FnOnce:對應 self 呼叫方法。僅執行一次的可呼叫 (callable) 類型,執行後不再使用(釋放捕捉)。
  • trait FnMut: FnOnce:對應 &mut self 呼叫方法。內含可變引用的可呼叫類型,可重複呼叫。
  • trait Fn: FnMut:對應 &self 呼叫方法。僅內含唯讀引用的可呼叫類型,可重複呼叫。

這三個 trait 擁有特殊語法,可使用類似函式指標的語法 Fn() -> bool 來描述參數與回傳值。

而 trait 邊界應該要反過來讀,滿足 Fn 的類型也可以滿足 FnMutFnOnce 的要求,而 FnOnce 的要求最寬鬆。一般靜態函式都符合 Fn() + 'static 這類最高級別的邊界,反過來說,如果閉包使用了可變引用,必定不符合 Fn 的要求;使用了任何引用,必定不符合 'static 的要求。

若要回傳閉包,必須使用 move 關鍵字移動所有捕捉的變數、標示該閉包的生命週期,並以最高級的邊界表示。

fn hello<'a>(name: &'a str) -> impl Fn() -> String + 'a {
    move || format!("Hello {name}!")
}

最後,枚舉 (enumeration) 類型的呼叫式變體 (varient) 其實也有實作 Fn trait,如 enum E { V(u8) }E::V 實作了 Fn(u8) -> E 邊界。

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment