溫馨提示×

您好,登錄后才能下訂單哦!

密碼登錄×
登錄注冊(cè)×
其他方式登錄
點(diǎn)擊 登錄注冊(cè) 即表示同意《億速云用戶服務(wù)條款》

如何理解Rust閉包的蟲洞穿梭

發(fā)布時(shí)間:2021-10-28 11:45:02 來源:億速云 閱讀:148 作者:iii 欄目:web開發(fā)

這篇文章主要介紹“如何理解Rust閉包的蟲洞穿梭”,在日常操作中,相信很多人在如何理解Rust閉包的蟲洞穿梭問題上存在疑惑,小編查閱了各式資料,整理出簡(jiǎn)單好用的操作方法,希望對(duì)大家解答”如何理解Rust閉包的蟲洞穿梭”的疑惑有所幫助!接下來,請(qǐng)跟著小編一起來學(xué)習(xí)吧!

1. 閉包是什么

閉包(Closure)的概念由來已久。無論哪種語言,閉包的概念都被以下幾個(gè)特征共同約束:

  • 匿名數(shù)函(非獨(dú)有,函數(shù)指針也可以);

  • 可以調(diào)用閉包,并顯式傳遞參數(shù)(非獨(dú)有,函數(shù)指針也可以);

  • 以變量形式存在,可以傳來傳去(非獨(dú)有,函數(shù)指針也可以);

  • 可以在閉包內(nèi)直接捕獲并使用定義所處作用域的值(獨(dú)有);

神奇的是最后一點(diǎn),理解起來也比較別扭的,習(xí)慣就好了。

為了說明上述特征,可以看一個(gè)Rust例子。

fn display<T>(age: u32, print_info: T)     where T: Fn(u32) {    print_info(age);}fn main() {     let name = String::from("Ethan");     let print_info_closure = |age|{         println!("name is {}", name);         println!("age is {}", age);     };    let age = 18;     display(age, print_info_closure);}

運(yùn)行代碼:

name is Ethan  age is 18

首先,閉包作為匿名函數(shù)存在了print_info_closure棧變量中,然后傳遞給了函數(shù)display作為參數(shù),在display內(nèi)部調(diào)用了閉包,并傳遞了參數(shù)age。最后神奇的事情出現(xiàn)了:在函數(shù)display中調(diào)用的閉包居然打印出了函數(shù)main作用域中的變量name。

如何理解Rust閉包的蟲洞穿梭

閉包的精髓,就在于它同時(shí)涉及兩個(gè)作用域,就仿佛打開了一個(gè)"蟲洞",讓不同作用域的變量穿梭其中。

let x_closure = ||{};

單獨(dú)一行代碼,就藏著這個(gè)奧妙:

  • 賦值=的左側(cè),是存儲(chǔ)閉包的變量,它處在一個(gè)作用域中,也就是我們說的閉包定義處的環(huán)境上下文;

  • 賦值=的右側(cè),那對(duì)花括號(hào){}里,也是一個(gè)作用域,它在閉包被調(diào)用處動(dòng)態(tài)產(chǎn)生;

無論左側(cè)右側(cè),都定義了閉包的屬性,天然的聯(lián)通了兩個(gè)作用域。

對(duì)于閉包,Rust如此,其他語言也大抵如此。不過,Rust不是還有所有權(quán)、生命周期這一檔子事兒么,所以還可以深入分析下。

2. Rust閉包捕獲上下文的方式

Rust閉包如何捕獲上下文?

換個(gè)問法,main作用域中的變量name是以何種方式進(jìn)入閉包的作用域的(第1節(jié)例子)?轉(zhuǎn)移or借用?

It Depends,視情況而定。

Rust在std中定義了3種trait:

  • FnOnce:閉包內(nèi)對(duì)外部變量存在轉(zhuǎn)移操作,導(dǎo)致外部變量不可用(所以只能call一次);

  • FnMut:閉包內(nèi)對(duì)外部變量直接使用,并進(jìn)行修改;

  • Fn:閉包內(nèi)對(duì)外部變量直接使用,不進(jìn)行修改;

后者能辦到的,前者一定能辦到。反之則不然。所以,編譯器對(duì)閉包簽名進(jìn)行推理時(shí):

  • 實(shí)現(xiàn)FnMut的,同時(shí)也實(shí)現(xiàn)了FnOnce;

  • 實(shí)現(xiàn)Fn的,同時(shí)也實(shí)現(xiàn)了FnMut和FnOnce。

第1節(jié)的例子,將display的泛型參數(shù)從Fn改成FnMut,也可以無警告通過。

fn display<T>(age: u32, mut print_info: T)     where T: FnMut(u32) {    print_info(age);}

對(duì)環(huán)境變量進(jìn)行捕獲的閉包,需要額外的空間支持才能將環(huán)境變量進(jìn)行存儲(chǔ)。

3. 作為參數(shù)的閉包簽名

上面代碼display函數(shù)定義,要接受一個(gè)閉包作為參數(shù),揭示了如何顯式的描述閉包的簽名:在泛型參數(shù)上添加trait約束,比如T:  FnMut(u32),其中(u32)顯式的表示了輸入?yún)?shù)的類型。盡管是泛型參數(shù)約束,但是函數(shù)簽名(除了沒有函數(shù)名)描述還是非常精確的。

順便說一句,Rust的泛型真的是干了不少事情,除了泛型該干的,還能添加trait約束,還能描述生命周期。

描述簽名是一回事,但是誰來定義閉包的簽名呢?閉包定義處,我們沒有看到任何的類型約束,直接就可以調(diào)用。

答案是:閉包的簽名,編譯器全部一手包辦了,它會(huì)將首次調(diào)用閉包傳入?yún)?shù)和返回值的類型,綁定到閉包的簽名。這就意味著,一旦閉包被調(diào)用過一次后,再次調(diào)用閉包時(shí)傳入的參數(shù)類型,就必須是和第一次相同。

傳入?yún)?shù)和返回值類型綁定好了,但你心中難免還會(huì)有一絲憂愁:描述生命周期的泛型參數(shù)腫么辦?

Rust編譯器也搞得定。

fn main(){         let lifttime_closure = |a, b|{         println!("{}", a);         println!("{}", b);         b    };    let a = String::from("abc");     let c;     {        let b = String::from("xyz");         c = lifttime_closure(&a, &b);    }    println!("{}", c); }

以上代碼無法通過編譯,成功檢測(cè)出了懸垂引用:

error[E0597]: b does not live long enough

顯然,對(duì)于閉包,編譯期可以對(duì)引用的生命周期進(jìn)行檢查,以保證引用始終有效。

這個(gè)例子,與其解釋閉包與函數(shù)的區(qū)別,不如解釋匿名函數(shù)與具名函數(shù)的區(qū)別:

  • 具名函數(shù)是簽名在先的,對(duì)于編譯器來說,調(diào)用方和函數(shù)內(nèi)部實(shí)現(xiàn),只要分別遵守簽名的約定即可。

  • 匿名函數(shù)的簽名則是被推理出來的,編譯器要看全看透調(diào)用方的實(shí)際輸入,以及函數(shù)內(nèi)部的實(shí)際返回,檢查自然也就順帶做掉了。

4. 函數(shù)返回閉包

第1節(jié)的例子,我們將一個(gè)閉包作為函數(shù)參數(shù)傳入,那么根據(jù)閉包的特性,它應(yīng)該能夠作為函數(shù)的返回值。答案是肯定的。

基于前面介紹的Fn trait,我們定義一個(gè)返回閉包的函數(shù),代碼如下:

fn closure_return() -> Fn() -> (){     ||{}    }

可是,編譯失敗了:

error[E0746]: return type cannot have an unboxed trait object  doesn't have a size known at compile-time

失敗信息顯示,編譯器無法確定函數(shù)返回值的大小。一個(gè)閉包有多大呢?并不重要。

開門見山,通用的解決方法是:為了能夠返回閉包,可以使用一次裝箱,從而將棧內(nèi)存變量裝箱存入堆內(nèi)存,這樣無論閉包有多大,函數(shù)返回值都是一個(gè)確定大小的指針。下面的代碼里,使用Box::new即可完成裝箱。

fn closure_inside() -> Box<dyn FnMut() -> ()> {    let mut age = 1;     let mut name = String::from("Ethan");     let age_closure = move || {         name.push_str(" Yuan");         age += 1;         println!("name is {}", name);         println!("age is {}", age);     };    Box::new(age_closure) }fn main(){     let mut age_closure = closure_inside();     age_closure();    age_closure();}

運(yùn)行結(jié)果如下:

  • name is Ethan Yuan

  • age is 2

  • name is Ethan Yuan Yuan

  • age is 3

上面的代碼,除了讓函數(shù)成功返回閉包之外,還有一個(gè)目的,我們想讓閉包捕獲函數(shù)內(nèi)部環(huán)境中的值,但這次有些不同:

  • 第1節(jié)代碼示例,我們把外層的環(huán)境上下文,通過將閉包傳入內(nèi)層函數(shù),這個(gè)不難理解,因?yàn)橥鈱幼兞康纳芷诟L(zhǎng),內(nèi)層函數(shù)訪問時(shí),外層變量還活著;

  • 而本節(jié)代碼所做的,是通過閉包將內(nèi)層函數(shù)的環(huán)境變量傳出來給外層環(huán)境;

內(nèi)層函數(shù)調(diào)用完成后就會(huì)銷毀內(nèi)層環(huán)境變量,那如何做到呢?幸好,Rust有所有權(quán)轉(zhuǎn)移。只要能促成內(nèi)層函數(shù)的環(huán)境變量向閉包進(jìn)行所有權(quán)的轉(zhuǎn)移,這個(gè)操作順理成章。

正因?yàn)镽ust具有所有權(quán)轉(zhuǎn)移的概念,返回閉包(同時(shí)捕獲環(huán)境變量)的機(jī)理,Rust的要比任何具有垃圾回收語言(JavaScript、Java、C#)的解釋都更簡(jiǎn)單明了。后者總會(huì)給人一絲不安:內(nèi)部函數(shù)調(diào)用都結(jié)束了,居然局部變量還活著。

代碼中的所有權(quán)轉(zhuǎn)移,這里使用了關(guān)鍵字move,它可以在構(gòu)建閉包時(shí),強(qiáng)制將要捕獲變量的所有權(quán)轉(zhuǎn)移至閉包內(nèi)部的特別存儲(chǔ)區(qū)。需要注意的是,使用move,并不影響閉包的trait,本例中可以看到閉包是FnMut,而不是FnOnce。

到此,關(guān)于“如何理解Rust閉包的蟲洞穿梭”的學(xué)習(xí)就結(jié)束了,希望能夠解決大家的疑惑。理論與實(shí)踐的搭配能更好的幫助大家學(xué)習(xí),快去試試吧!若想繼續(xù)學(xué)習(xí)更多相關(guān)知識(shí),請(qǐng)繼續(xù)關(guān)注億速云網(wǎng)站,小編會(huì)繼續(xù)努力為大家?guī)砀鄬?shí)用的文章!

向AI問一下細(xì)節(jié)

免責(zé)聲明:本站發(fā)布的內(nèi)容(圖片、視頻和文字)以原創(chuàng)、轉(zhuǎn)載和分享為主,文章觀點(diǎn)不代表本網(wǎng)站立場(chǎng),如果涉及侵權(quán)請(qǐng)聯(lián)系站長(zhǎng)郵箱:is@yisu.com進(jìn)行舉報(bào),并提供相關(guān)證據(jù),一經(jīng)查實(shí),將立刻刪除涉嫌侵權(quán)內(nèi)容。

AI