閉包又稱匿名函式 (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);
只要是函式內沒有宣告、也非參數的變數被引用,就會啟動「捕捉」。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;
};
前面已提過,閉包其實為一種專用的結構體搭配專用函式,所以沒有固定的類型名稱(會以亂數產生)。靜態閉包可以像靜態函式有 fn() -> bool
這種已知類型的函式指標 (function pointer),但是其餘除外。在 Rust 中,只能以泛型描述非靜態的閉包,有下列三種:
trait FnOnce
:對應self
呼叫方法。僅執行一次的可呼叫 (callable) 類型,執行後不再使用(釋放捕捉)。trait FnMut: FnOnce
:對應&mut self
呼叫方法。內含可變引用的可呼叫類型,可重複呼叫。trait Fn: FnMut
:對應&self
呼叫方法。僅內含唯讀引用的可呼叫類型,可重複呼叫。
這三個 trait 擁有特殊語法,可使用類似函式指標的語法 Fn() -> bool
來描述參數與回傳值。
而 trait 邊界應該要反過來讀,滿足 Fn
的類型也可以滿足 FnMut
、FnOnce
的要求,而 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
邊界。