原文標題:Understanding Closures in Rust
原文鏈接:https://medium.com/swlh/understanding-closures-in-rust-21f286ed1759
公眾號: Rust 碎碎念
翻譯 by: Praying
概要
-
閉包(closure)是函數指針(function pointer)和上下文(context)的組合。 -
沒有上下文的閉包就是一個函數指針。 -
帶有不可變上下文(immutable context)的閉包屬於 Fn
-
帶有可變上下文(mutable context)的閉包屬於 FnMut
-
擁有其上下文的閉包屬於 FnOnce
理解 Rust 中不同類型的閉包

不同於其他語言,Rust 對self
參數的使用是顯式的。當我們實現結構體時,必須把self
作為函數簽名的第一個參數:
struct MyStruct {
text: &'static str,
number: u32,
}
impl MyStruct {
fn new (text: &'static str, number: u32) -> MyStruct {
MyStruct {
text: text,
number: number,
}
}
// We have to specify that 'self' is an argument.
fn get_number (&self) -> u32 {
self.number
}
// We can specify different kinds of ownership and mutability of self.
fn inc_number (&mut self) {
self.number += 1;
}
// There are three different types of 'self'
fn destructor (self) {
println!("Destructing {}", self.text);
}
}
因此,下面這兩種風格是一樣的:
obj.get_number();
MyStruct::get_number(&obj);
這和那些把self
(或this
)隱藏起來的語言不同。在那些語言中,只要將一個函數和一個對象或結構關聯起來,就隱含着第一個參數是self
。在上面的代碼中,我們有四個self
選項:一個不可變引用,一個可變引用,一個被擁有的值,或者壓根就沒有使用self
作為參數。
因此,self
表示函數執行的某一類上下文。它在 Rust 中是顯式的,但是在其他語言中經常是隱含的。
此外,在本文中,我們將會使用下面的函數:
fn is_fn <A, R>(_x: fn(A) -> R) {}
fn is_Fn <A, R, F: Fn(A) -> R> (_x: &F) {}
fn is_FnMut <A, R, F: FnMut(A) -> R> (_x: &F) {}
fn is_FnOnce <A, R, F: FnOnce(A) -> R> (_x: &F) {}
這些函數的唯一作用是類型檢查。例如,如果is_FnMut(&func)
能夠編譯,那么我們就可以知道那個func
屬於FnMut
trait。
無上下文和fn
(小寫的 f)類型
鑒於上述內容,考慮幾個使用(上面定義的)MyStruct
的閉包的例子:
let obj1 = MyStruct::new("Hello", 15);
let obj2 = MyStruct::new("More Text", 10);
let closure1 = |x: &MyStruct| x.get_number() + 3;
assert_eq!(closure1(&obj1), 18);
assert_eq!(closure1(&obj2), 13);
這是我們可以得到的最簡單的(代碼示例)。這個閉包為類型MyStruct
的任意對象中的已有數字加上三。它可以在任意位置被執行,不會有任何問題,並且編譯器不會給你帶來任何麻煩。我們可以很簡單地寫出下面這樣的代碼替代closure1
:
// It doesn't matter what code appears here, the function will behave
// exactly the same.
fn func1 (x: &MyStruct) -> u32 {
x.get_number() + 3
}
assert_eq!(func1(&obj1), 18);
assert_eq!(func1(&obj2), 13);
這個函數不依賴於它的上下文。無論它之前和之后發生了什么,它的行為都是一致的。我們(幾乎)可以互換着使用func1
和closure1
。
當一個閉包完全不依賴上下文時,我們的閉包的類型就是fn
:
// compiles successfully.
is_fn(closure1);
is_Fn(&closure1);
is_FnMut(&closure1);
is_FnOnce(&closure1);
可變上下文和Fn
(大寫的 F)trait
相較於上面,我們可以為閉包添加一個上下文。
let obj1 = MyStruct::new("Hello", 15);
let obj2 = MyStruct::new("More Text", 10);
// obj1 is borrowed by the closure immutably.
let closure2 = |x: &MyStruct| x.get_number() + obj1.get_number();
assert_eq!(closure2(&obj2), 25);
// We can borrow obj1 again immutably...
assert_eq!(obj1.get_number(), 15);
// But we can't borrow it mutably.
// obj1.inc_number(); // ERROR
closure2
依賴於obj1
的值,並且包含周圍作用域的信息。在這段代碼中,closure2
將會借用obj1
從而使得它可以在函數體中使用obj1
。我們還可以對obj1
進行不可變借用,但是如果我們試圖在后面修改obj1
,我們將會得到一個借用錯誤。
如果我們嘗試使用fn
語法來重寫我們的閉包,我們在函數內部需要知道的一切都必須作為參數傳遞給函數,所以我們添加了一個額外的參數來表示函數的上下文:
struct Context<'a>(&'a MyStruct);
let obj1 = MyStruct::new("Hello", 15);
let obj2 = MyStruct::new("More Text", 10);
let ctx = Context(&obj1);
fn func2 (context: &Context, x: &MyStruct) -> u32 {
x.get_number() + context.0.get_number()
}
其行為和我們的閉包基本一致:
assert_eq!(func2(&ctx, &obj2), 25);
// We can borrow obj1 again immutably...
assert_eq!(obj1.get_number(), 15);
// But we can't borrow it mutably.
// obj1.inc_number(); // ERROR
注意,Context
結構體包含一個對MyStruct
結構體的不可變引用,這表明我們將無法在函數內部修改它。
當我們調用closure1
時,也意味着我們我們把周圍的上下文作為參數傳遞給了closure1
,正如我們在使用fn
時必須要做的那樣。在一些其他的語言中,我們不必指定將self
作為參數傳遞,與之類似,Rust 也不需要我們顯式地指定將上下文作為參數傳遞。
當一個閉包以不可變引用的方式捕獲上下文,我們稱它實現了Fn
trait。這表明我們可以不必修改上下文而多次調用我們的函數。
// Does not compile:
// is_fn(closure2);
// Compiles successfully:
is_Fn(&closure2);
is_FnMut(&closure2);
is_FnOnce(&closure2);
可變上下文和FnMut
trait
如果我們在閉包內部修改obj1
,我們會得到不同的結果:
let mut obj1 = MyStruct::new("Hello", 15);
let obj2 = MyStruct::new("More Text", 10);
// obj1 is borrowed by the closure mutably.
let mut closure3 = |x: &MyStruct| {
obj1.inc_number();
x.get_number() + obj1.get_number()
};
assert_eq!(closure3(&obj2), 26);
assert_eq!(closure3(&obj2), 27);
assert_eq!(closure3(&obj2), 28);
// We can't borrow obj1 mutably or immutably
// assert_eq!(obj1.get_number(), 18); // ERROR
// obj1.inc_number(); // ERROR
這一次我們不能對obj1
進行可變借用和不可變借用了。我們還必須得把閉包標注為mut
。如果我們希望使用fn
語法重寫這個函數,將會得到下面的代碼:
struct Context<'a>(&'a mut MyStruct);
let mut obj1 = MyStruct::new("Hello", 15);
let obj2 = MyStruct::new("More Text", 10);
let mut ctx = Context(&mut obj1);
// obj1 is borrowed by the closure mutably.
fn func3 (context: &mut Context, x: &MyStruct) -> u32 {
context.0.inc_number();
x.get_number() + context.0.get_number()
};
其行為與closure3
相同:
assert_eq!(func3(&mut ctx, &obj2), 26);
assert_eq!(func3(&mut ctx, &obj2), 27);
assert_eq!(func3(&mut ctx, &obj2), 28);
// We can't borrow obj1 mutably or immutably
// assert_eq!(obj1.get_number(), 18); // ERROR
// obj1.inc_number(); // ERROR
注意,我們必須把我們的上下文以可變引用的方式傳遞。這表明每次調用我們的函數后,可能會得到不同的結果。
當閉包以可變引用捕獲它的上下文時,我們稱它屬於FnMut
trait。
// Does not compile:
// is_fn(closure3);
// is_Fn(&closure3);
// Compiles successfully:
is_FnMut(&closure3);
is_FnOnce(&closure3);
擁有的上下文
在我們的最后一個例子中,我們將會獲取obj1
的所有權:
let obj1 = MyStruct::new("Hello", 15);
let obj2 = MyStruct::new("More Text", 10);
// obj1 is owned by the closure
let closure4 = |x: &MyStruct| {
obj1.destructor();
x.get_number()
};
我們必須在使用closure4
之前檢查它的類型:
// Does not compile:
// is_fn(closure4);
// is_Fn(&closure4);
// is_FnMut(&closure4);
// Compiles successfully:
is_FnOnce(&closure4);
現在我們可以檢查它的行為:
assert_eq!(closure4(&obj2), 10);
// We can't call closure4 twice...
// assert_eq!(closure4(&obj2), 10); //ERROR
// We can't borrow obj1 mutably or immutably
// assert_eq!(obj1.get_number(), 15); // ERROR
// obj1.inc_number(); // ERROR
在這個例子中,我們只能調用這個函數一次。一旦我們對它進行了第一次調用,obj1
就被我們銷毀了, 所以它在第二次調用的時候也就不復存在了。Rust 會給我們一個關於使用一個已經被移動的值的錯誤。這就是為什么我們要事先檢查其類型。
使用fn
語法來實現,我們可以得到下面的代碼:
struct Context(MyStruct);
let obj1 = MyStruct::new("Hello", 15);
let obj2 = MyStruct::new("More Text", 10);
let ctx = Context(obj1);
// obj1 is owned by the closure
fn func4 (context: Context, x: &MyStruct) -> u32 {
context.0.destructor();
x.get_number()
};
這段代碼,正如我們所預期的,和我們的閉包行為一致:
assert_eq!(func4(ctx, &obj2), 10);
// We can't call func4 twice...
// assert_eq!(func4(ctx, &obj2), 10); //ERROR
// We can't borrow obj1 mutably or immutably
// assert_eq!(obj1.get_number(), 15); // ERROR
// obj1.inc_number(); // ERROR
當我們使用fn
來寫我們的閉包時,我們必須使用一個Context
結構體並擁有它的值。當一個閉包擁有其上下文的所有權時,我們稱它實現了FnOnce
。我們只能調用這個函數一次,因為在調用之后,上下文就被銷毀了。
總結
-
不需要上下文的函數擁有
fn
類型,並且可以在任意位置調用。 -
僅需要不可變地訪問其上下文的函數屬於
Fn
trait,並且只要上下文在作用域中存在,就可以在任意位置調用。 -
需要可變地訪問其上下文的函數實現了
FnMut
trait,可以在上下文有效的任意位置調用,但是每次調用可能會做不同的事情。 -
獲取上下文所有權的函數只能被調用一次。
