Rust 不僅命名函數(shù),還命名匿名函數(shù)。匿名函數(shù)有一個(gè)關(guān)聯(lián)的環(huán)境被稱為“閉包”,因?yàn)樗麄冴P(guān)閉了一個(gè)環(huán)境。如我們將會(huì)看到的,Rust 對(duì)它們有完美的實(shí)現(xiàn)。
閉包看起來(lái)是這樣的:
let plus_one = |x: i32| x + 1;
assert_eq!(2, plus_one(1));
我們創(chuàng)建一個(gè)綁定,plus_one 并將其分配給一個(gè)閉包。關(guān)閉管道之間的參數(shù)(|),并且主體是一個(gè)表達(dá)式,在本例中是 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 定義的普通函數(shù)略有不同。第一個(gè)不同點(diǎn)是,我們不需要對(duì)參數(shù)的類型和返回值進(jìn)行注釋。我們可以:
let plus_one = |x: i32| -> i32 { x + 1 };
assert_eq!(2, plus_one(1));
但我們并不需要這樣做。這是為什么呢?根本上來(lái)說(shuō),這是出于人體工程學(xué)的考慮。對(duì)于文檔和類型推斷而言,指定已命名函數(shù)的完整類型命名是有用的,然而閉包的類型很少被記錄因?yàn)樗麄兪悄涿?,而且他們不?huì)引 error-at-a-distance 錯(cuò)誤,這種錯(cuò)誤能夠推斷命名函數(shù)的類型。
第二,語(yǔ)法是相似的,但有點(diǎn)不同。我在這里添加空格讓他們看起來(lái)更接近:
ffn 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 ;
差異很小,但它們類似。
閉包之所以被稱為閉包,是因?yàn)樗麄兎忾]了他們的環(huán)境。它看起來(lái)像這樣:
let num = 5;
let plus_num = |x: i32| x + num;
assert_eq!(10, plus_num(5));
plus_num,這個(gè)閉包指的是一個(gè) let 綁定在它的范圍 num 內(nèi)。更具體地說(shuō),它借用了綁定。如果我們做與綁定相沖突的事情,就會(huì)得到一個(gè)錯(cuò)誤。像下面這樣:
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;
}
^
這里的錯(cuò)誤信息雖然冗長(zhǎng)卻是有用的!比如說(shuō),我們不能對(duì) num 進(jìn)行一個(gè)可變的 borrow,因?yàn)殚]包已經(jīng) borrow 過(guò)它了。如果我們讓閉包超出范圍,我們可以:
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 將獲取所有權(quán)并且移動(dòng)環(huán)境:
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<i32>]`, which is non-copyable
let takes_nums = || nums;
^~~~~~~
Vec < T >
對(duì)其內(nèi)容擁有所有權(quán),因此,當(dāng)我們?cè)陂]包的操作涉及到它時(shí),我們將不得不聲稱對(duì) num 的所有權(quán)。同樣的,如果我們將 num 傳遞給一個(gè)函數(shù),則這個(gè)函數(shù)對(duì)其擁有所有權(quán)。
我們可以強(qiáng)制我們的閉包用 move 關(guān)鍵字獲取環(huán)境移所有權(quán):
let num = 5;
let owns_num = move |x: i32| x + num;
現(xiàn)在,即使關(guān)鍵字是 move,變量仍然遵循正常 move語(yǔ)義。在這種情況下,5 實(shí)現(xiàn)復(fù)制,所以 owns_num 持有 num 的副本。然而區(qū)別在哪里呢?
let mut num = 5;
{
let mut add_num = |x: i32| num += x;
add_num(5);
}
assert_eq!(10, num);
所以在這種情況下,我們的閉包獲得了一個(gè)可變 num,我們稱為 add_num, 如我們所期望的,它改變了 num 的潛在值。我們還需要將 add_nu m聲明為 mut,因?yàn)槲覀冋诟淖兤洵h(huán)境。
如果我們換成一個(gè) move 閉包,就會(huì)出現(xiàn)不同:
let mut num = 5;
{
let mut add_num = move |x: i32| num += x;
add_num(5);
}
assert_eq!(5, num);
我們只得到 5。而不是從 num 得到可變的 borrow 我們對(duì)副本擁有所有權(quán)。
另一種方式思考 mov e閉包:他們分配給閉包一個(gè)自己的堆棧幀。沒(méi)有 move 一個(gè)閉包可能與創(chuàng)建它的堆棧幀聯(lián)系到一起,而且閉包是自包含的。這意味著你不能從函數(shù)返回一個(gè) non-move 閉包。
但在我們討論使用和返回閉包并之前,我們應(yīng)該更多的討論閉包的實(shí)現(xiàn)方式。作為一種系統(tǒng)語(yǔ)言、Rust 讓你能夠控制代碼所做的事情,而閉包是沒(méi)有什么不同的。
Rust 的閉包實(shí)現(xiàn)有點(diǎn)不同于其他語(yǔ)言。對(duì)特征來(lái)說(shuō)他們是非常高效的語(yǔ)言。你要確保閱讀這一章之前已經(jīng)閱讀了特征這一章,以及特征對(duì)象這一章。
都明白了嗎?很棒。
閉包工作的關(guān)鍵點(diǎn)有些奇怪:使用()調(diào)用一個(gè)函數(shù),就像 foo() 是一種可重載操作符。由此,在其他的任何地方單擊鼠標(biāo)都能進(jìn)入空間。在 Rust 語(yǔ)言里面,我們使用特征系統(tǒng)重載操作符。調(diào)用函數(shù)也不例外。我們有三個(gè)獨(dú)立的過(guò)載與特征:
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ì)注意到這些特征之間的差異,但很大的一個(gè)點(diǎn)是 self:Fn 和 &self,FnMut 和 &mut self,FnOnce 和 self。這通過(guò)常用的方法調(diào)用語(yǔ)法涵蓋了所有三種 self。但是我們把他們分成三個(gè)特征,而不是一個(gè)。這給了我們足夠的控制權(quán)去決定我們可以采用什么樣的閉包。
對(duì)閉包的| | { }語(yǔ)法對(duì)這三個(gè)特征來(lái)說(shuō)是糖衣語(yǔ)法。Rus t將為環(huán)境生成一個(gè) struct, impl 適當(dāng)?shù)奶卣?然后使用它。
現(xiàn)在我們知道,閉包特征,我們已經(jīng)知道如何接受和返回閉包:就像任何其他特征那樣!
這也意味著我們可以選擇靜態(tài)與動(dòng)態(tài)調(diào)度。首先,讓我們寫一個(gè)可以調(diào)用其他函數(shù)的函數(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);
我們通過(guò)閉包,| | x + 2
去調(diào)用 call_with_one。它只是做它所表明的事情:它調(diào)用閉包,1 作為參數(shù)。
讓我們更深入地檢查 call_with_one 的簽名:
fn call_with_one < F >(some_closure:F)- >i32
我們需要一個(gè)參數(shù),它的類型為 F .我們也返回一個(gè) i32。這部分不是很有趣。下一個(gè)部分是:
where F : Fn(i32) -> i32 {
因?yàn)?Fn 是一個(gè)特征,我們可以將我們的泛型和它綁定到一起。在這種情況下,我們的閉包需要把 i32 作為參數(shù),并返回一個(gè) i32 所以我們使用的泛型邊界是 Fn(i32) -> i32
。
這里還有一個(gè)關(guān)鍵問(wèn)題:因?yàn)槲覀儼煞盒秃吞卣鹘壎ǖ搅艘黄?,這將導(dǎo)致單形態(tài),因此,我們將在閉包里面做靜態(tài)調(diào)度。那是就清晰多了。在許多語(yǔ)言中,閉包本身就是堆分配,總是涉及到動(dòng)態(tài)調(diào)度。在 Rust 語(yǔ)言中,我們可以用堆棧分配我們的閉包環(huán)境,和靜態(tài)調(diào)度 call 語(yǔ)句。這種情況通常發(fā)生于迭代器和適配器,通常采用閉包作為參數(shù)。
當(dāng)然,如果我們想要?jiǎng)討B(tài)調(diào)度,我們也可以那樣。特征對(duì)象處理這種情況時(shí),像往常一樣:
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)在我們把一個(gè)特征對(duì)象,&Fn
。當(dāng)我們傳遞這個(gè)特征到 call_with_one 時(shí)我們必須參考我們的閉包,所以我們使用 & | |
。
在各種情況下,返回閉包是很常見的函數(shù)形式。如果你想返回一個(gè)閉包,您可能會(huì)遇到一個(gè)錯(cuò)誤。起初,這看起來(lái)可能有些奇怪,但是我們會(huì)找到答案。你可以試著從下面的函數(shù)返回一個(gè)閉包:
fn factory() -> (Fn(i32) -> Vec<i32>) {
let vec = vec![1, 2, 3];
|n| vec.push(n)
}
let f = factory();
let answer = f(4);
assert_eq!(vec![1, 2, 3, 4], answer);
error: the trait `core::marker::Sized` is not implemented for the type
`core::ops::Fn(i32) -> collections::vec::Vec<i32>` [E0277]
f = factory();
^
note: `core::ops::Fn(i32) -> collections::vec::Vec<i32>` does not have a
constant size known at compile-time
f = factory();
^
error: the trait `core::marker::Sized` is not implemented for the type
`core::ops::Fn(i32) -> collections::vec::Vec<i32>` [E0277]
factory() -> (Fn(i32) -> Vec<i32>) {
^~~~~~~~~~~~~~~~~~~~~
note: `core::ops::Fn(i32) -> collections::vec::Vec<i32>` does not have a constant size known at compile-time
fa ctory() -> (Fn(i32) -> Vec<i32>) {
^~~~~~~~~~~~~~~~~~~~~
為了從一個(gè)函數(shù)返回一些東西,Rust 需要知道返回類型的大小。但由于 Fn 是一個(gè)特征,它可能具有是各種不同大小的 size :
許多不同的類型都可以實(shí)現(xiàn) Fn。來(lái)給一些事物賦予 siz e的一種簡(jiǎn)單的方法是參考已知的 size。所以如下面我們寫的這樣:
fn factory() -> &(Fn(i32) -> Vec<i32>) {
let vec = vec![1, 2, 3];
|n| vec.push(n)
}
let f = factory();
let answer = f(4);
assert_eq!(vec![1, 2, 3, 4], answer);
但我們得到了另一個(gè)錯(cuò)誤:
error: missing lifetime specifier [E0106]
fn factory() -> &(Fn(i32) -> i32) {
^~~~~~~~~~~~~~~~~
正確的。因?yàn)槲覀冇幸粋€(gè)參考,我們需要給它指定生命周期。但是我們的 factory() 函數(shù)不帶參數(shù),所以此處省略不寫。我們可以選擇什么樣的生命周期呢?靜態(tài):
fn factory() -> &'static (Fn(i32) -> i32) {
let num = 5;
|x| x + num
}
let f = factory();
let answer = f(1);
assert_eq!(6, answer);
但我們得到另一個(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ò)誤是讓我們知道,我們沒(méi)有 &'static Fn(i32) -> i32
但是我們有一個(gè)[closure <anon>:7:9: 7:20]
。等等,這是什么?
因?yàn)槊總€(gè)閉包生成自己的環(huán)境結(jié)構(gòu)和實(shí)現(xiàn) Fn 特征以及 friends 這些都是匿名的。它們的存在只是因?yàn)檫@個(gè)閉包。Rust 把他們顯示為 closure <anon>
而不是一些自動(dòng)生成的名字。
但是為什么我們的閉包不實(shí)現(xiàn) &'static Fn
?正如我們之前討論的,閉包 borrow 了他們的環(huán)境。在這種情況下,我們的環(huán)境是基于 5 棧分配以及num 變量綁定。因此,borrow 的生命周期為的堆棧幀長(zhǎng)度。所以如果我們返回這個(gè)閉包,函數(shù)調(diào)用將結(jié)束,堆??蚣軐⑾?,并且我們的閉包將對(duì)垃圾內(nèi)存的環(huán)境進(jìn)行捕獲!
那么該怎么辦?這就是工作原理:
fn factory() -> Box<Fn(i32) -> i32> {
let num = 5;
Box::new(|x| x + num)
}
let f = factory();
let answer = f(1);
assert_eq!(6, answer);
我們使用一個(gè)特征對(duì)象,通過(guò)建立 Fn。只有最后一個(gè)問(wèn)題:
error: `num` does not live long enough
Box::new(|x| x + num)
^~~~~~~~~~~
我們?nèi)匀挥幸粋€(gè)對(duì)父堆棧幀的引用。通過(guò)最后一次修復(fù),我們這樣做可以做可以行得通:
fn factory() -> Box<Fn(i32) -> i32> {
let num = 5;
Box::new(move |x| x + num)
}
let f = factory();
let answer = f(1);
assert_eq!(6, answer);
通過(guò)內(nèi)部 move Fn,我們可以為我們的閉包創(chuàng)建一個(gè)新的堆棧幀。我們給它一個(gè)已知大小,并對(duì)它進(jìn)行填充,而且允許他脫離我們的堆棧幀。
更多建議: