鍍金池/ 教程/ Java/ 閉包
哲學(xué)家就餐問題
鏈接進(jìn)階
名詞中英文對(duì)照
測(cè)試
引用和借用
泛型
方法語法
函數(shù)
不安全代碼
并發(fā)
裝箱語法和模式
注釋
棧和堆
運(yùn)算符與重載
語法索引
文檔
固有功能
所有權(quán)
循環(huán)
通用函數(shù)調(diào)用語法
不定長(zhǎng)類型
<code>const</code> 和 <code>static</code>
迭代器
其他語言中的 Rust
枚舉
詞匯表
If語句
猜猜看
錯(cuò)誤處理
生命周期
編譯器插件
發(fā)布途徑
閉包
trait 對(duì)象
不使用標(biāo)準(zhǔn)庫
關(guān)聯(lián)常量
外部函數(shù)接口(FFI)
類型轉(zhuǎn)換
原生類型
匹配
參考文獻(xiàn)
Rust 編程語言
內(nèi)聯(lián)匯編
條件編譯
選擇你的保證
學(xué)習(xí) Rust
`type`別名
自定義內(nèi)存分配器
屬性
if let
高效 Rust
可變性
語法和語義
模式
基準(zhǔn)測(cè)試
結(jié)構(gòu)體
變量綁定
語言項(xiàng)
切片模式
<code>Deref</code> 強(qiáng)制多態(tài)
關(guān)聯(lián)類型
裸指針
<code>Borrow</code> 和 <code>AsRef</code>
準(zhǔn)備
Rust 開發(fā)版
字符串

閉包

closures.md
commit 6ba952020fbc91bad64be1ea0650bfba52e6aab4

有時(shí)為了整潔和復(fù)用打包一個(gè)函數(shù)和自由變量(free variables)是很有用的。自由變量是指被用在函數(shù)中來自函數(shù)內(nèi)部作用域并只用于函數(shù)內(nèi)部的變量。對(duì)此,我們用一個(gè)新名字“閉包”而且 Rust 提供了大量關(guān)于他們的實(shí)現(xiàn),正如我們將看到的。

語法

閉包看起來像這樣:

let plus_one = |x: i32| x + 1;

assert_eq!(2, plus_one(1));

我們創(chuàng)建了一個(gè)綁定,plus_one,并把它賦予一個(gè)閉包。閉包的參數(shù)位于管道(|)之中,而閉包體是一個(gè)表達(dá)式,在這個(gè)例子中,x + 1。記住{}是一個(gè)表達(dá)式,所以我們也可以擁有包含多行的閉包:

let plus_two = |x| {
    let mut result: i32 = x;

    result += 1;
    result += 1;

    result
};

assert_eq!(4, plus_two(2));

你會(huì)注意到閉包的一些方面與用fn定義的常規(guī)函數(shù)有點(diǎn)不同。第一個(gè)是我們并不需要標(biāo)明閉包接收和返回參數(shù)的類型。我們可以:

let plus_one = |x: i32| -> i32 { x + 1 };

assert_eq!(2, plus_one(1));

不過我們并不需要這么寫。為什么呢?基本上,這是出于“人體工程學(xué)”的原因。因?yàn)闉槊瘮?shù)指定全部類型有助于像文檔和類型推斷,而閉包的類型則很少有文檔因?yàn)樗鼈兪悄涿?,并且并不?huì)產(chǎn)生像推斷一個(gè)命名函數(shù)的類型這樣的“遠(yuǎn)距離錯(cuò)誤”。

第二個(gè)是語法是相似的,不過有點(diǎn)不同。我會(huì)增加空格來使它們看起來更像一點(diǎn):

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  ;

有些小區(qū)別,不過仍然是相似的。

閉包及環(huán)境

之所以把它稱為“閉包”是因?yàn)樗鼈儭鞍诃h(huán)境中”(close over their environment)。這看起來像:

let num = 5;
let plus_num = |x: i32| x + num;

assert_eq!(10, plus_num(5));

這個(gè)閉包,plus_num,引用了它作用域中的let綁定:num。更明確的說,它借用了綁定。如果我們做一些會(huì)與這個(gè)綁定沖突的事,我們會(huì)得到一個(gè)錯(cuò)誤。比如這個(gè):

let mut num = 5;
let plus_num = |x: i32| x + num;

let y = &mut num;

錯(cuò)誤是:

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;
}
^

一個(gè)啰嗦但有用的錯(cuò)誤信息!如它所說,我們不能取得一個(gè)num的可變借用因?yàn)殚]包已經(jīng)借用了它。如果我們讓閉包離開作用域,我們可以:

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會(huì)取得所有權(quán)并移動(dòng)環(huán)境。這個(gè)不能工作:

let nums = vec![1, 2, 3];

let takes_nums = || nums;

println!("{:?}", nums);

這會(huì)給我們:

note: `nums` moved into closure environment here because it has type
  `[closure(()) -> collections::vec::Vec<i32>]`, which is non-copyable
let takes_nums = || nums;
                    ^~~~~~~

Vec<T>擁有它內(nèi)容的所有權(quán),而且由于這個(gè)原因,當(dāng)我們?cè)陂]包中引用它時(shí),我們必須取得nums的所有權(quán)。這與我們傳遞nums給一個(gè)取得它所有權(quán)的函數(shù)一樣。

move閉包

我們可以使用move關(guān)鍵字強(qiáng)制使我們的閉包取得它環(huán)境的所有權(quán):

let num = 5;

let owns_num = move |x: i32| x + num;

現(xiàn)在,即便關(guān)鍵字是move,變量遵循正常的移動(dòng)語義。在這個(gè)例子中,5實(shí)現(xiàn)了Copy,所以owns_num取得一個(gè)5的拷貝的所有權(quán)。那么區(qū)別是什么呢?

let mut num = 5;

{
    let mut add_num = |x: i32| num += x;

    add_num(5);
}

assert_eq!(10, num);

那么在這個(gè)例子中,我們的閉包取得了一個(gè)num的可變引用,然后接著我們調(diào)用了add_num,它改變了其中的值,正如我們期望的。我們也需要將add_num聲明為mut,因?yàn)槲覀儠?huì)改變它的環(huán)境。

如果我們改為一個(gè)move閉包,這有些不同:

let mut num = 5;

{
    let mut add_num = move |x: i32| num += x;

    add_num(5);
}

assert_eq!(5, num);

我們只會(huì)得到5。與其獲取一個(gè)我們num的可變借用,我們?nèi)〉昧艘粋€(gè)拷貝的所有權(quán)。

另一個(gè)理解move閉包的方法:它給出了一個(gè)擁有自己棧幀的閉包。沒有move,一個(gè)閉包可能會(huì)綁定在創(chuàng)建它的棧幀上,而move閉包則是獨(dú)立的。例如,這意味著大體上你不能從函數(shù)返回一個(gè)非move閉包。

不過在我們討論獲取或返回閉包之前,我們應(yīng)該更多的了解一下閉包實(shí)現(xiàn)的方法。作為一個(gè)系統(tǒng)語言,Rust給予你了大量的控制你代碼的能力,而閉包也是一樣。

閉包實(shí)現(xiàn)

Rust 的閉包實(shí)現(xiàn)與其它語言有些許不用。它們實(shí)際上是trait的語法糖。在此之前你可能要確定已經(jīng)讀過trait章節(jié)和[trait對(duì)象](Trait Objects trait 對(duì)象.md)。

都讀過了?很好。

理解閉包底層是如何工作的關(guān)鍵有點(diǎn)奇怪:使用()調(diào)用函數(shù),像foo(),是一個(gè)可重載的運(yùn)算符。到此,其它的一切都會(huì)明了。在Rust中,我們使用trait系統(tǒng)來重載運(yùn)算符。調(diào)用函數(shù)也不例外。我們有三個(gè)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;
}
# }

你會(huì)注意到這些 trait 之間的些許區(qū)別,不過一個(gè)大的區(qū)別是selfFn獲取&selfFnMut獲取&mut self,而FnOnce獲取self。這包含了所有3種通過通常函數(shù)調(diào)用語法的self。不過我們將它們分在 3 個(gè) trait 里,而不是單獨(dú)的 1 個(gè)。這給了我們大量的對(duì)于我們可以使用哪種閉包的控制。

閉包的|| {}語法是上面 3 個(gè) trait 的語法糖。Rust 將會(huì)為了環(huán)境創(chuàng)建一個(gè)結(jié)構(gòu)體,impl合適的 trait,并使用它。

閉包作為參數(shù)(Taking closures as arguments)

現(xiàn)在我們知道了閉包是 trait,我們已經(jīng)知道了如何接受和返回閉包;就像任何其它的 trait!

這也意味著我們也可以選擇靜態(tài)或動(dòng)態(tài)分發(fā)。首先,讓我們寫一個(gè)函數(shù),它接受可調(diào)用的參數(shù),調(diào)用之,然后返回結(jié)果:

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。它正做了我們說的:它調(diào)用了閉包,1作為參數(shù)。

讓我們更深層的解析call_with_one的簽名:

fn call_with_one<F>(some_closure: F) -> i32
#    where F : Fn(i32) -> i32 {
#    some_closure(1) }

我們獲取一個(gè)參數(shù),而它有類型F。我們也返回一個(gè)i32。這一部分并不有趣。下一部分是:

# fn call_with_one<F>(some_closure: F) -> i32
    where F : Fn(i32) -> i32 {
#   some_closure(1) }

因?yàn)?code>Fn是一個(gè)trait,我們可以用它限制我們的泛型。在這個(gè)例子中,我們的閉包取得一個(gè)i32作為參數(shù)并返回i32,所以我們用泛型限制是Fn(i32) -> i32。

還有一個(gè)關(guān)鍵點(diǎn)在于:因?yàn)槲覀冇靡粋€(gè)trait限制泛型,它會(huì)是單態(tài)的,并且因此,我們?cè)陂]包中使用靜態(tài)分發(fā)。這是非常簡(jiǎn)單的。在很多語言中,閉包固定在堆上分配,所以總是進(jìn)行動(dòng)態(tài)分發(fā)。在Rust中,我們可以在棧上分配我們閉包的環(huán)境,并靜態(tài)分發(fā)調(diào)用。這經(jīng)常發(fā)生在迭代器和它們的適配器上,它們經(jīng)常取得閉包作為參數(shù)。

當(dāng)然,如果我們想要?jiǎng)討B(tài)分發(fā),我們也可以做到。trait對(duì)象處理這種情況,通常:

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);

現(xiàn)在我們?nèi)〉靡粋€(gè)trait對(duì)象,一個(gè)&Fn。并且當(dāng)我們將我們的閉包傳遞給call_with_one時(shí)我們必須獲取一個(gè)引用,所以我們使用&||

函數(shù)指針和閉包

一個(gè)函數(shù)指針有點(diǎn)像一個(gè)沒有環(huán)境的閉包。因此,你可以傳遞函數(shù)指針給任何期待閉包參數(shù)的函數(shù),且能夠工作:

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);

在這個(gè)例子中,我們并不是嚴(yán)格的需要這個(gè)中間變量f,函數(shù)的名字就可以了:

let answer = call_with_one(&add_one);

返回閉包(Returning closures)

對(duì)于函數(shù)式風(fēng)格代碼來說在各種情況返回閉包是非常常見的。如果你嘗試返回一個(gè)閉包,你可能會(huì)得到一個(gè)錯(cuò)誤。在剛接觸的時(shí)候,這看起來有點(diǎn)奇怪,不過我們會(huì)搞清楚。當(dāng)你嘗試從函數(shù)返回一個(gè)閉包的時(shí)候,你可能會(huì)寫出類似這樣的代碼:

fn factory() -> (Fn(i32) -> i32) {
    let num = 5;

    |x| x + num
}

let f = factory();

let answer = f(1);
assert_eq!(6, answer);

編譯的時(shí)候會(huì)給出這一長(zhǎng)串相關(guān)錯(cuò)誤:

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();
    ^

為了從函數(shù)返回一些東西,Rust 需要知道返回類型的大小。不過Fn是一個(gè) trait,它可以是各種大小(size)的任何東西。比如說,返回值可以是實(shí)現(xiàn)了Fn的任意類型。一個(gè)簡(jiǎn)單的解決方法是:返回一個(gè)引用。因?yàn)橐玫拇笮?size)是固定的,因此返回值的大小就固定了。因此我們可以這樣寫:

fn factory() -> &(Fn(i32) -> i32) {
    let num = 5;

    |x| x + num
}

let f = factory();

let answer = f(1);
assert_eq!(6, answer);

不過這樣會(huì)出現(xiàn)另外一個(gè)錯(cuò)誤:

error: missing lifetime specifier [E0106]
fn factory() -> &(Fn(i32) -> i32) {
                ^~~~~~~~~~~~~~~~~

對(duì)。因?yàn)槲覀冇幸粋€(gè)引用,我們需要給它一個(gè)生命周期。不過我們的factory()函數(shù)不接收參數(shù),所以省略不能用在這。我們可以使用什么生命周期呢?'static

fn factory() -> &'static (Fn(i32) -> i32) {
    let num = 5;

    |x| x + num
}

let f = factory();

let answer = f(1);
assert_eq!(6, answer);

不過這樣又會(huì)出現(xiàn)另一個(gè)錯(cuò)誤:

error: mismatched types:
 expected `&'static core::ops::Fn(i32) -> i32`,
    found `[closure@<anon>:7:9: 7:20]`
(expected &-ptr,
    found closure) [E0308]
         |x| x + num
         ^~~~~~~~~~~

這個(gè)錯(cuò)誤讓我們知道我們并沒有返回一個(gè)&'static Fn(i32) -> i32,而是返回了一個(gè)[closure <anon>:7:9: 7:20]。等等,什么?

因?yàn)槊總€(gè)閉包生成了它自己的環(huán)境struct并實(shí)現(xiàn)了Fn和其它一些東西,這些類型是匿名的。它們只在這個(gè)閉包中存在。所以Rust把它們顯示為closure <anon>,而不是一些自動(dòng)生成的名字。

這個(gè)錯(cuò)誤也指出了返回值類型期望是一個(gè)引用,不過我們嘗試返回的不是。更進(jìn)一步,我們并不能直接給一個(gè)對(duì)象'static聲明周期。所以我們換一個(gè)方法并通過Box裝箱Fn來返回一個(gè) trait 對(duì)象。這個(gè)幾乎可以成功運(yùn)行:

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);
# }

這還有最后一個(gè)問題:

error: closure may outlive the current function, but it borrows `num`,
which is owned by the current function [E0373]
Box::new(|x| x + num)
         ^~~~~~~~~~~

好吧,正如我們上面討論的,閉包借用他們的環(huán)境。而且在這個(gè)例子中。我們的環(huán)境基于一個(gè)棧分配的5,num變量綁定。所以這個(gè)借用有這個(gè)棧幀的生命周期。所以如果我們返回了這個(gè)閉包,這個(gè)函數(shù)調(diào)用將會(huì)結(jié)束,棧幀也將消失,那么我們的閉包獲得了被釋放的內(nèi)存環(huán)境!再有最后一個(gè)修改,我們就可以讓它運(yùn)行了:

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);
# }

通過把內(nèi)部閉包變?yōu)?code>move Fn,我們?yōu)殚]包創(chuàng)建了一個(gè)新的棧幀。通過Box裝箱,我們提供了一個(gè)已知大小的返回值,并允許它離開我們的棧幀。

上一篇:準(zhǔn)備下一篇:語言項(xiàng)