# 閉包
> [closures.md](https://github.com/rust-lang/rust/blob/master/src/doc/book/closures.md)
commit 6ba952020fbc91bad64be1ea0650bfba52e6aab4
有時為了整潔和復用打包一個函數和*自由變量(free variables)*是很有用的。自由變量是指被用在函數中來自函數內部作用域并只用于函數內部的變量。對此,我們用一個新名字“閉包”而且 Rust 提供了大量關于他們的實現,正如我們將看到的。
### 語法
閉包看起來像這樣:
~~~
let plus_one = |x: i32| x + 1;
assert_eq!(2, plus_one(1));
~~~
我們創建了一個綁定,`plus_one`,并把它賦予一個閉包。閉包的參數位于管道(`|`)之中,而閉包體是一個表達式,在這個例子中,`x + 1`。記住`{}`是一個表達式,所以我們也可以擁有包含多行的閉包:
~~~
let plus_two = |x| {
let mut result: i32 = x;
result += 1;
result += 1;
result
};
assert_eq!(4, plus_two(2));
~~~
你會注意到閉包的一些方面與用`fn`定義的常規函數有點不同。第一個是我們并不需要標明閉包接收和返回參數的類型。我們可以:
~~~
let plus_one = |x: i32| -> i32 { x + 1 };
assert_eq!(2, plus_one(1));
~~~
不過我們并不需要這么寫。為什么呢?基本上,這是出于“人體工程學”的原因。因為為命名函數指定全部類型有助于像文檔和類型推斷,而閉包的類型則很少有文檔因為它們是匿名的,并且并不會產生像推斷一個命名函數的類型這樣的“遠距離錯誤”。
第二個是語法是相似的,不過有點不同。我會增加空格來使它們看起來更像一點:
~~~
fn plus_one_v1 (x: i32) -> i32 { x + 1 }
let plus_one_v2 = |x: i32| -> i32 { x + 1 };
let plus_one_v3 = |x: i32| x + 1 ;
~~~
有些小區別,不過仍然是相似的。
### 閉包及環境
之所以把它稱為“閉包”是因為它們“包含在環境中”(close over their environment)。這看起來像:
~~~
let num = 5;
let plus_num = |x: i32| x + num;
assert_eq!(10, plus_num(5));
~~~
這個閉包,`plus_num`,引用了它作用域中的`let`綁定:`num`。更明確的說,它借用了綁定。如果我們做一些會與這個綁定沖突的事,我們會得到一個錯誤。比如這個:
~~~
let mut num = 5;
let plus_num = |x: i32| x + num;
let y = &mut num;
~~~
錯誤是:
~~~
error: cannot borrow `num` as mutable because it is also borrowed as immutable
let y = &mut num;
^~~
note: previous borrow of `num` occurs here due to use in closure; the immutable
borrow prevents subsequent moves or mutable borrows of `num` until the borrow
ends
let plus_num = |x| x + num;
^~~~~~~~~~~
note: previous borrow ends here
fn main() {
let mut num = 5;
let plus_num = |x| x + num;
let y = &mut num;
}
^
~~~
一個啰嗦但有用的錯誤信息!如它所說,我們不能取得一個`num`的可變借用因為閉包已經借用了它。如果我們讓閉包離開作用域,我們可以:
~~~
let mut num = 5;
{
let plus_num = |x: i32| x + num;
} // plus_num goes out of scope, borrow of num ends
let y = &mut num;
~~~
然而,如果你的閉包需要它,Rust會取得所有權并移動環境。這個不能工作:
~~~
let nums = vec![1, 2, 3];
let takes_nums = || nums;
println!("{:?}", nums);
~~~
這會給我們:
~~~
note: `nums` moved into closure environment here because it has type
`[closure(()) -> collections::vec::Vec]`, which is non-copyable
let takes_nums = || nums;
^~~~~~~
~~~
`Vec<T>`擁有它內容的所有權,而且由于這個原因,當我們在閉包中引用它時,我們必須取得`nums`的所有權。這與我們傳遞`nums`給一個取得它所有權的函數一樣。
### `move`閉包
我們可以使用`move`關鍵字強制使我們的閉包取得它環境的所有權:
~~~
let num = 5;
let owns_num = move |x: i32| x + num;
~~~
現在,即便關鍵字是`move`,變量遵循正常的移動語義。在這個例子中,`5`實現了`Copy`,所以`owns_num`取得一個`5`的拷貝的所有權。那么區別是什么呢?
~~~
let mut num = 5;
{
let mut add_num = |x: i32| num += x;
add_num(5);
}
assert_eq!(10, num);
~~~
那么在這個例子中,我們的閉包取得了一個`num`的可變引用,然后接著我們調用了`add_num`,它改變了其中的值,正如我們期望的。我們也需要將`add_num`聲明為`mut`,因為我們會改變它的環境。
如果我們改為一個`move`閉包,這有些不同:
~~~
let mut num = 5;
{
let mut add_num = move |x: i32| num += x;
add_num(5);
}
assert_eq!(5, num);
~~~
我們只會得到`5`。與其獲取一個我們`num`的可變借用,我們取得了一個拷貝的所有權。
另一個理解`move`閉包的方法:它給出了一個擁有自己棧幀的閉包。沒有`move`,一個閉包可能會綁定在創建它的棧幀上,而`move`閉包則是獨立的。例如,這意味著大體上你不能從函數返回一個非`move`閉包。
不過在我們討論獲取或返回閉包之前,我們應該更多的了解一下閉包實現的方法。作為一個系統語言,Rust給予你了大量的控制你代碼的能力,而閉包也是一樣。
### 閉包實現
Rust 的閉包實現與其它語言有些許不用。它們實際上是trait的語法糖。在此之前你可能要確定已經讀過[trait章節](#)和[trait對象](#)。
都讀過了?很好。
理解閉包底層是如何工作的關鍵有點奇怪:使用`()`調用函數,像`foo()`,是一個可重載的運算符。到此,其它的一切都會明了。在Rust中,我們使用trait系統來重載運算符。調用函數也不例外。我們有三個trait來分別重載:
~~~
# mod foo {
pub trait Fn<Args> : FnMut<Args> {
extern "rust-call" fn call(&self, args: Args) -> Self::Output;
}
pub trait FnMut<Args> : FnOnce<Args> {
extern "rust-call" fn call_mut(&mut self, args: Args) -> Self::Output;
}
pub trait FnOnce<Args> {
type Output;
extern "rust-call" fn call_once(self, args: Args) -> Self::Output;
}
# }
~~~
你會注意到這些 trait 之間的些許區別,不過一個大的區別是`self`:`Fn`獲取`&self`,`FnMut`獲取`&mut self`,而`FnOnce`獲取`self`。這包含了所有3種通過通常函數調用語法的`self`。不過我們將它們分在 3 個 trait 里,而不是單獨的 1 個。這給了我們大量的對于我們可以使用哪種閉包的控制。
閉包的`|| {}`語法是上面 3 個 trait 的語法糖。Rust 將會為了環境創建一個結構體,`impl`合適的 trait,并使用它。
### 閉包作為參數(Taking closures as arguments)
現在我們知道了閉包是 trait,我們已經知道了如何接受和返回閉包;就像任何其它的 trait!
這也意味著我們也可以選擇靜態或動態分發。首先,讓我們寫一個函數,它接受可調用的參數,調用之,然后返回結果:
~~~
fn call_with_one<F>(some_closure: F) -> i32
where F : Fn(i32) -> i32 {
some_closure(1)
}
let answer = call_with_one(|x| x + 2);
assert_eq!(3, answer);
~~~
我們傳遞我們的閉包,`|x| x + 2`,給`call_with_one`。它正做了我們說的:它調用了閉包,`1`作為參數。
讓我們更深層的解析`call_with_one`的簽名:
~~~
fn call_with_one<F>(some_closure: F) -> i32
# where F : Fn(i32) -> i32 {
# some_closure(1) }
~~~
我們獲取一個參數,而它有類型`F`。我們也返回一個`i32`。這一部分并不有趣。下一部分是:
~~~
# fn call_with_one<F>(some_closure: F) -> i32
where F : Fn(i32) -> i32 {
# some_closure(1) }
~~~
因為`Fn`是一個trait,我們可以用它限制我們的泛型。在這個例子中,我們的閉包取得一個`i32`作為參數并返回`i32`,所以我們用泛型限制是`Fn(i32) -> i32`。
還有一個關鍵點在于:因為我們用一個trait限制泛型,它會是單態的,并且因此,我們在閉包中使用靜態分發。這是非常簡單的。在很多語言中,閉包固定在堆上分配,所以總是進行動態分發。在Rust中,我們可以在棧上分配我們閉包的環境,并靜態分發調用。這經常發生在迭代器和它們的適配器上,它們經常取得閉包作為參數。
當然,如果我們想要動態分發,我們也可以做到。trait對象處理這種情況,通常:
~~~
fn call_with_one(some_closure: &Fn(i32) -> i32) -> i32 {
some_closure(1)
}
let answer = call_with_one(&|x| x + 2);
assert_eq!(3, answer);
~~~
現在我們取得一個trait對象,一個`&Fn`。并且當我們將我們的閉包傳遞給`call_with_one`時我們必須獲取一個引用,所以我們使用`&||`。
### 函數指針和閉包
一個函數指針有點像一個沒有環境的閉包。因此,你可以傳遞函數指針給任何期待閉包參數的函數,且能夠工作:
~~~
fn call_with_one(some_closure: &Fn(i32) -> i32) -> i32 {
some_closure(1)
}
fn add_one(i: i32) -> i32 {
i + 1
}
let f = add_one;
let answer = call_with_one(&f);
assert_eq!(2, answer);
~~~
在這個例子中,我們并不是嚴格的需要這個中間變量`f`,函數的名字就可以了:
~~~
let answer = call_with_one(&add_one);
~~~
### 返回閉包(Returning closures)
對于函數式風格代碼來說在各種情況返回閉包是非常常見的。如果你嘗試返回一個閉包,你可能會得到一個錯誤。在剛接觸的時候,這看起來有點奇怪,不過我們會搞清楚。當你嘗試從函數返回一個閉包的時候,你可能會寫出類似這樣的代碼:
~~~
fn factory() -> (Fn(i32) -> i32) {
let num = 5;
|x| x + num
}
let f = factory();
let answer = f(1);
assert_eq!(6, answer);
~~~
編譯的時候會給出這一長串相關錯誤:
~~~
error: the trait `core::marker::Sized` is not implemented for the type
`core::ops::Fn(i32) -> i32` [E0277]
fn factory() -> (Fn(i32) -> i32) {
^~~~~~~~~~~~~~~~
note: `core::ops::Fn(i32) -> i32` does not have a constant size known at compile-time
fn factory() -> (Fn(i32) -> i32) {
^~~~~~~~~~~~~~~~
error: the trait `core::marker::Sized` is not implemented for the type `core::ops::Fn(i32) -> i32` [E0277]
let f = factory();
^
note: `core::ops::Fn(i32) -> i32` does not have a constant size known at compile-time
let f = factory();
^
~~~
為了從函數返回一些東西,Rust 需要知道返回類型的大小。不過`Fn`是一個 trait,它可以是各種大小(size)的任何東西。比如說,返回值可以是實現了`Fn`的任意類型。一個簡單的解決方法是:返回一個引用。因為引用的大小(size)是固定的,因此返回值的大小就固定了。因此我們可以這樣寫:
~~~
fn factory() -> &(Fn(i32) -> i32) {
let num = 5;
|x| x + num
}
let f = factory();
let answer = f(1);
assert_eq!(6, answer);
~~~
不過這樣會出現另外一個錯誤:
~~~
error: missing lifetime specifier [E0106]
fn factory() -> &(Fn(i32) -> i32) {
^~~~~~~~~~~~~~~~~
~~~
對。因為我們有一個引用,我們需要給它一個生命周期。不過我們的`factory()`函數不接收參數,所以省略不能用在這。我們可以使用什么生命周期呢?`'static`:
~~~
fn factory() -> &'static (Fn(i32) -> i32) {
let num = 5;
|x| x + num
}
let f = factory();
let answer = f(1);
assert_eq!(6, answer);
~~~
不過這樣又會出現另一個錯誤:
~~~
error: mismatched types:
expected `&'static core::ops::Fn(i32) -> i32`,
found `[closure@:7:9: 7:20]`
(expected &-ptr,
found closure) [E0308]
|x| x + num
^~~~~~~~~~~
~~~
這個錯誤讓我們知道我們并沒有返回一個`&'static Fn(i32) -> i32`,而是返回了一個`[closure <anon>:7:9: 7:20]`。等等,什么?
因為每個閉包生成了它自己的環境`struct`并實現了`Fn`和其它一些東西,這些類型是匿名的。它們只在這個閉包中存在。所以Rust把它們顯示為`closure <anon>`,而不是一些自動生成的名字。
這個錯誤也指出了返回值類型期望是一個引用,不過我們嘗試返回的不是。更進一步,我們并不能直接給一個對象`'static`聲明周期。所以我們換一個方法并通過`Box`裝箱`Fn`來返回一個 trait 對象。這個*幾乎*可以成功運行:
~~~
fn factory() -> Box<Fn(i32) -> i32> {
let num = 5;
Box::new(|x| x + num)
}
# fn main() {
let f = factory();
let answer = f(1);
assert_eq!(6, answer);
# }
~~~
這還有最后一個問題:
~~~
error: closure may outlive the current function, but it borrows `num`,
which is owned by the current function [E0373]
Box::new(|x| x + num)
^~~~~~~~~~~
~~~
好吧,正如我們上面討論的,閉包借用他們的環境。而且在這個例子中。我們的環境基于一個棧分配的`5`,`num`變量綁定。所以這個借用有這個棧幀的生命周期。所以如果我們返回了這個閉包,這個函數調用將會結束,棧幀也將消失,那么我們的閉包獲得了被釋放的內存環境!再有最后一個修改,我們就可以讓它運行了:
~~~
fn factory() -> Box<Fn(i32) -> i32> {
let num = 5;
Box::new(move |x| x + num)
}
# fn main() {
let f = factory();
let answer = f(1);
assert_eq!(6, answer);
# }
~~~
通過把內部閉包變為`move Fn`,我們為閉包創建了一個新的棧幀。通過`Box`裝箱,我們提供了一個已知大小的返回值,并允許它離開我們的棧幀。
- 前言
- 貢獻者
- 1.介紹
- 2.準備
- 3.學習 Rust
- 3.1.猜猜看
- 3.2.哲學家就餐問題
- 3.3.其它語言中的 Rust
- 4.語法和語義
- 4.1.變量綁定
- 4.2.函數
- 4.3.原生類型
- 4.4.注釋
- 4.5.If語句
- 4.6.循環
- 4.7.所有權
- 4.8.引用和借用
- 4.9.生命周期
- 4.10.可變性
- 4.11.結構體
- 4.12.枚舉
- 4.13.匹配
- 4.14.模式
- 4.15.方法語法
- 4.16.Vectors
- 4.17.字符串
- 4.18.泛型
- 4.19.Traits
- 4.20.Drop
- 4.21.if let
- 4.22.trait 對象
- 4.23.閉包
- 4.24.通用函數調用語法
- 4.25.crate 和模塊
- 4.26.const和static
- 4.27.屬性
- 4.28.type別名
- 4.29.類型轉換
- 4.30.關聯類型
- 4.31.不定長類型
- 4.32.運算符和重載
- 4.33.Deref強制多態
- 4.34.宏
- 4.35.裸指針
- 4.36.不安全代碼
- 5.高效 Rust
- 5.1.棧和堆
- 5.2.測試
- 5.3.條件編譯
- 5.4.文檔
- 5.5.迭代器
- 5.6.并發
- 5.7.錯誤處理
- 5.8.選擇你的保證
- 5.9.外部函數接口
- 5.10.Borrow 和 AsRef
- 5.11.發布途徑
- 5.12.不使用標準庫
- 6.Rust 開發版
- 6.1.編譯器插件
- 6.2.內聯匯編
- 6.4.固有功能
- 6.5.語言項
- 6.6.鏈接進階
- 6.7.基準測試
- 6.8.裝箱語法和模式
- 6.9.切片模式
- 6.10.關聯常量
- 6.11.自定義內存分配器
- 7.詞匯表
- 8.語法索引
- 9.參考文獻
- 附錄:名詞中英文對照