原文標題: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,可以在上下文有效的任意位置呼叫,但是每次呼叫可能會做不同的事情。獲取上下文所有權的函式只能被呼叫一次。