今天就跟大家聊聊有關Rust中怎么實現(xiàn)閉包,可能很多人都不太了解,為了讓大家更加了解,小編給大家總結了以下內容,希望大家根據(jù)這篇文章可以有所收獲。
讓客戶滿意是我們工作的目標,不斷超越客戶的期望值來自于我們對這個行業(yè)的熱愛。我們立志把好的技術通過有效、簡單的方式提供給客戶,將通過不懈努力成為客戶在信息化領域值得信任、有價值的長期合作伙伴,公司提供的服務項目有:域名與空間、虛擬空間、營銷軟件、網(wǎng)站建設、山東網(wǎng)站維護、網(wǎng)站推廣。
閉包的基本語法
閉包,又稱為lambda表達式,可以像函數(shù)一樣被調用,但具有使用外部環(huán)境中的變量的能力,其中外部環(huán)境是指閉包定義時所在的作用域。一個典型的閉包如下所示:
let x = 1; let add = |a: i32| -> i32 { return a + x; }; println!("{}", add(2)); // 輸出:3
可以看到,add
是一個閉包,它有一個參數(shù),用兩個|
包圍,執(zhí)行語句包含在{}
中,閉包的參數(shù)和返回值類型的指定與普通函數(shù)的語法相同,但可以省略。若{}
中只包含一條語句,{}
也可以省略。也就是說,add
可以簡寫為如下形式:
let add = |a| a + x;
在閉包add
中,它可以使用外部環(huán)境中的變量x
,這是和普通函數(shù)最大的不同。需要注意的是,對于普通函數(shù)fn
來說,其無需前向聲明,但閉包需要先定義后使用,從這個角度講閉包更像是一個變量,而且它具有和變量同樣的“生命周期”。
下面我們來看一下閉包是如何使用外部環(huán)境中的變量的。
對每一個閉包,編譯器會自動生成一個匿名struct類型,并通過分析閉包的內部邏輯來決定該結構體包括哪些數(shù)據(jù)以及數(shù)據(jù)該如何初始化,如果閉包中使用了外部環(huán)境變量,則結構體中會包括該變量。從這個層面講,閉包其實是一種語法糖。對于上面提到的閉包add
,編譯器會將其自動轉化為如下形式(只是舉個例子,并非真正的編譯器處理閉包的方式):
struct Closure { x: i32, } impl Closure { fn call(&self, a: i32) -> i32 { self.x + a } } fn main() { let x = 1; let add = Closure { x: x }; println!("{}", add.call(2)); // 輸出:3 }
可以看到,若想在閉包中使用一個外部環(huán)境中的變量,需要分兩步:第一步是構造相應的結構體并捕獲外部環(huán)境變量,就如let add = Closure { x: x };
所示;第二步是構造結構體的成員函數(shù)并使用外部環(huán)境變量,就如fn call(&self, a: i32) -> i32
所示。在這兩個步驟中,還需要考慮兩個問題:
第一個問題關心的是閉包如何捕獲外部變量:結構體內部的成員應當使用什么類型呢,是T
、&T
還是&mut T
呢?
第二個問題關心的是閉包如何使用外部變量:函數(shù)調用的self
應當使用什么類型呢,是self
、&self
還是&mut self
呢?
對于閉包如何捕獲外部變量,編譯器的原則是“按需捕獲”:在保證能編譯通過的情況下,結構體內部的成員優(yōu)先選擇&T
,其次是&mut T
,最后選擇T
。這個原則的核心就是“選擇對結構體外部影響最小的存儲類型”,具體來說就是:
如果一個外部變量在閉包中只通過借用指針&
使用,那么這個變量就可使用&
捕獲;
如果一個外部變量在閉包中通過可變借用指針&mut
使用,那么這個變量就需要使用&mut
捕獲;
如果一個外部變量在閉包中通過所有權轉移方式使用過,那么這個變量就需要使用T
捕獲;
看下面這個例子:
struct T(i32); fn by_move(_: T) {} fn by_ref(_: &T) {} fn by_mut(_: &mut T) {} fn main() { let x = T(1); let y = T(2); let mut z = T(3); let closure = || { by_move(x); by_ref(&y); by_mut(&mut z); }; closure(); }
以上閉包分別以T
、&T
還是&mut T
的方式捕獲了外部的變量x
,y
,z
,所以編譯器會自動生成類似于下面這樣的結構體:
struct Closure { x: T, y: &T, z: &mut T, }
需要注意的是,如果承載閉包的變量不再是局部變量,而是被傳遞出了當前作用域,則該閉包必須選擇傳遞所有權的方式才能保證編譯通過,這時可以使用move
關鍵字修飾閉包,強制將閉包中變量的捕獲全部使用所有權轉移的方式。例如:
fn make_adder(x: i32) -> Box<Fn(i32) -> i32> { Box::new(move |y| x + y) }
可以看到,局部變量x
被傳遞出了函數(shù)外,如果不加move
關鍵字,編譯器會提示:
error[E0373]: closure may outlive the current function, but it borrows `x`, which is owned by the current function
閉包使用外部變量的方式將影響相應的結構體成員函數(shù)的第一個參數(shù)self
的類型,對于self
、&self
和&mut self
三種類型,Rust提供了三個trait對其進行抽象。這三個trait是Fn
、FnMut
和FnOnce
,它們的定義如下:
pub trait FnOnce<Args> { type Output; extern "rust-call" fn call_once(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 Fn<Args>: FnMut<Args> { extern "rust-call" fn call(&self, args: Args) -> Self::Output; }
可以看到,這三個trait的區(qū)別就在于self
的類型不同:
FnOnce
的調用參數(shù)為self
,表示閉包通過轉移所有權的方式來使用外部環(huán)境中的變量,所以該閉包只能調用一次;
FnMut
的調用參數(shù)是&mut self
,表示閉包通過可變借用的方式來使用外部環(huán)境中的變量;
Fn
的調用參數(shù)是&self
,表示閉包通過不可變借用的方式來使用外部環(huán)境中的變量;
需要注意的是,Fn
繼承自FnMut
,FnMut
繼承自FnOnce
,這意味著,如果要實現(xiàn)Fn
,就必須實現(xiàn)FnMut
和FnOnce
;如果要實現(xiàn)FnMut
,就必須實現(xiàn)FnOnce
。這里面蘊含的邏輯是,如果該閉包能夠以Fn
方式調用,那么它也一定能以FnMut
和FnOnce
方式調用。
看下面這個例子:
fn main() { let s = "hello".to_string(); let use_by_ref = move || { println!("{}", s); }; use_by_ref(); // Fn use_by_ref(); let mut s = "hello".to_string(); let mut use_by_mut = || { s.push_str(" world"); println!("{}", s); }; use_by_mut(); // FnMut use_by_mut(); let s = "hello".to_string(); let use_by_move = || { drop(s); }; use_by_move(); // FnOnce // use_by_move(); // error[E0382]: use of moved value: `use_by_move` }
可以看到,閉包use_by_ref
是只讀方式使用了外部變量s
,如果不使用move
關鍵字,它的捕獲方式是&T
,使用方式是Fn
,這里為了演示,強制使用move
關鍵字將捕獲方式改為T
,但由于閉包中對變量s
的使用仍然是只讀,所以使用方式仍然是Fn
,而不是FnOnce
;閉包use_by_mut
是可變方式使用了外部變量s
,它的捕獲方式是&mut T
,使用方式是FnMut
;閉包use_bu_move
是所有權轉移方式使用了外部變量s
,它的捕獲方式是T
,使用方式是FnOnce
,所以在第二次調用use_by_move
時會報錯。
閉包一定會實現(xiàn)Fn
、FnMut
和FnOnce
三種trait之一,所以可以將閉包作為函數(shù)參數(shù)和返回類型,但需要注意的是,不要忘記trait是一種DST類型,它的大小在編譯階段是不固定的,從而不能直接作為參數(shù)類型或者返回值類型,這也是Rust中的trait和其他語言中的接口的重大區(qū)別之一。請看下面的例子,在函數(shù)tset
中不可以直接使用Bird
作為類型,編譯器會報錯。
trait Bird { fn fly(&self); } struct Duck; struct Swan; impl Bird for Duck { fn fly(&self) { println!("duck duck"); } } impl Bird for Swan { fn fly(&self) { println!("swan swan"); } } // error[E0277]: the size for values of type `(dyn Bird + 'static)` cannot // be known at compilation time fn test(arg: Bird) {}
難道我們在Rust中就不能擁有多態(tài)了嗎?那是不可能的,我們有兩種選擇:
靜態(tài)分派:通過泛型的方式,為不同的泛型類型參數(shù)生成不同版本的函數(shù),實現(xiàn)編譯期靜態(tài)分派。
fn test<T: Bird>(arg: T) { arg.fly(); }
動態(tài)分派,通過trait objetc的方式,將閉包裝箱進入堆內存中,函數(shù)傳遞的是一個胖指針,從實現(xiàn)運行期動態(tài)分派。
fn test(arg: Box<dyn Bird>) { arg.fly(); }
下面我們來具體介紹一下靜態(tài)分派和動態(tài)分派。
對于閉包,其泛型參數(shù)的寫法有一些特殊之處,如下面代碼所示:
fn call_with_closure<F>(closure: F) -> i32 where F: Fn(i32) -> i32, { closure(1) }
其中泛型參數(shù)F
的約束條件是F: Fn(i32) -> i32
,這使得看起來和普通函數(shù)類型更相似從而更易閱讀。
使用泛型的方式在函數(shù)參數(shù)中可以正常使用,但卻無法將一個閉包作為函數(shù)值返回,因為Rust中只支持函數(shù)返回具體類型,而閉包是一個匿名類型,這使得編譯器無法自動推斷且程序員也無法手動指定。這時,可以使用impl trait
語法糖,看下面的例子:
fn multiply(m: i32) -> impl Fn(i32) -> i32 { move |x| x * m }
這里的impl Fn(i32) -> i32
表示,這個返回類型,雖然我們不知道它的具體名字,但是知道它滿足Fn(i32) -> i32
這個trait的約束。這個功能目前還不是特別穩(wěn)定,建議不要激進使用,推薦使用下面介紹的動態(tài)分派的方式來解決返回值的問題。
動態(tài)分派是通過指針的方式來實現(xiàn)多態(tài),雖然trait是的DST,但指向trait的指針不是DST。如果我們把trait隱藏到指針的后面,那么就稱它是一個trait object,而trait object是可以作為參數(shù)和返回類型的。
指向trait的指針就是trait object。假如Bird
是一個trait的名稱,那么dyn Bird
就是一個DST動態(tài)大小類型,則&dyn Bird
、Box<dyn Bird>
以及Rc<dyn Bird>
等都是trait object。當指針指向trait的時候,它就變成一個胖指針了,比如Box<dyn Bird>
可以理解為:
pub struct TraitObject { pub data: *mut(), pub vtable: *mut(), }
它里面包含了兩個指針,第一個表示地址,第二個指向“虛函數(shù)表”,里面有我們需要調用的具體函數(shù)的地址。這和C++中的虛函數(shù)表內存布局有所不同。在C++中,如果一個類型里面有虛函數(shù),則每一個這種類型的變量內部都包含一個指向虛函數(shù)表的地址。而在Rust中,對象本身不包含指向虛函數(shù)表的指針,這個指針是存在于trait object指針里面的,如果一個類型實現(xiàn)了多個trait,那么不同的trait object指向的虛函數(shù)表也不一樣。
需要注意的是,并不是所有的trait都可以構造trait object,trait objetc的構造是受到許多約束的。滿足以下條件的trait不能構造trait object:
當trait有Self: Sized
約束時
當函數(shù)除了第一個參數(shù)外,有Self
類型作為參數(shù)或返回類型時
當函數(shù)的第一個參數(shù)不是Self
時
當函數(shù)有泛型參數(shù)時
對于后三種情況,如果想把不滿足條件的函數(shù)剔除在外,可以為該函數(shù)加上Self: Sized
約束,例如fn foo(&self) where Self: Sized
。
看完上述內容,你們對Rust中怎么實現(xiàn)閉包有進一步的了解嗎?如果還想了解更多知識或者相關內容,請關注創(chuàng)新互聯(lián)行業(yè)資訊頻道,感謝大家的支持。
當前名稱:Rust中怎么實現(xiàn)閉包
網(wǎng)頁鏈接:http://aaarwkj.com/article42/ihpshc.html
成都網(wǎng)站建設公司_創(chuàng)新互聯(lián),為您提供關鍵詞優(yōu)化、外貿建站、網(wǎng)站改版、App開發(fā)、外貿網(wǎng)站建設、虛擬主機
聲明:本網(wǎng)站發(fā)布的內容(圖片、視頻和文字)以用戶投稿、用戶轉載內容為主,如果涉及侵權請盡快告知,我們將會在第一時間刪除。文章觀點不代表本網(wǎng)站立場,如需處理請聯(lián)系客服。電話:028-86922220;郵箱:631063699@qq.com。內容未經(jīng)允許不得轉載,或轉載時需注明來源: 創(chuàng)新互聯(lián)