Rust中怎么實(shí)現(xiàn)閉包

今天就跟大家聊聊有關(guān)Rust中怎么實(shí)現(xiàn)閉包,可能很多人都不太了解,為了讓大家更加了解,小編給大家總結(jié)了以下內(nèi)容,希望大家根據(jù)這篇文章可以有所收獲。

讓客戶滿意是我們工作的目標(biāo),不斷超越客戶的期望值來(lái)自于我們對(duì)這個(gè)行業(yè)的熱愛(ài)。我們立志把好的技術(shù)通過(guò)有效、簡(jiǎn)單的方式提供給客戶,將通過(guò)不懈努力成為客戶在信息化領(lǐng)域值得信任、有價(jià)值的長(zhǎng)期合作伙伴,公司提供的服務(wù)項(xiàng)目有:域名與空間、虛擬空間、營(yíng)銷軟件、網(wǎng)站建設(shè)、山東網(wǎng)站維護(hù)、網(wǎng)站推廣。

閉包的基本語(yǔ)法

閉包,又稱為lambda表達(dá)式,可以像函數(shù)一樣被調(diào)用,但具有使用外部環(huán)境中的變量的能力,其中外部環(huán)境是指閉包定義時(shí)所在的作用域。一個(gè)典型的閉包如下所示:

let x = 1;
let add = |a: i32| -> i32 {
	return a + x;
};
println!("{}", add(2)); // 輸出:3

可以看到,add是一個(gè)閉包,它有一個(gè)參數(shù),用兩個(gè)|包圍,執(zhí)行語(yǔ)句包含在{}中,閉包的參數(shù)和返回值類型的指定與普通函數(shù)的語(yǔ)法相同,但可以省略。若{}中只包含一條語(yǔ)句,{}也可以省略。也就是說(shuō),add可以簡(jiǎn)寫為如下形式:

let add = |a| a + x;

在閉包add中,它可以使用外部環(huán)境中的變量x,這是和普通函數(shù)最大的不同。需要注意的是,對(duì)于普通函數(shù)fn來(lái)說(shuō),其無(wú)需前向聲明,但閉包需要先定義后使用,從這個(gè)角度講閉包更像是一個(gè)變量,而且它具有和變量同樣的“生命周期”。

下面我們來(lái)看一下閉包是如何使用外部環(huán)境中的變量的。

閉包的實(shí)現(xiàn)原理

對(duì)每一個(gè)閉包,編譯器會(huì)自動(dòng)生成一個(gè)匿名struct類型,并通過(guò)分析閉包的內(nèi)部邏輯來(lái)決定該結(jié)構(gòu)體包括哪些數(shù)據(jù)以及數(shù)據(jù)該如何初始化,如果閉包中使用了外部環(huán)境變量,則結(jié)構(gòu)體中會(huì)包括該變量。從這個(gè)層面講,閉包其實(shí)是一種語(yǔ)法糖。對(duì)于上面提到的閉包add,編譯器會(huì)將其自動(dòng)轉(zhuǎn)化為如下形式(只是舉個(gè)例子,并非真正的編譯器處理閉包的方式):

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
}

可以看到,若想在閉包中使用一個(gè)外部環(huán)境中的變量,需要分兩步:第一步是構(gòu)造相應(yīng)的結(jié)構(gòu)體并捕獲外部環(huán)境變量,就如let add = Closure { x: x };所示;第二步是構(gòu)造結(jié)構(gòu)體的成員函數(shù)并使用外部環(huán)境變量,就如fn call(&self, a: i32) -> i32所示。在這兩個(gè)步驟中,還需要考慮兩個(gè)問(wèn)題:

  • 第一個(gè)問(wèn)題關(guān)心的是閉包如何捕獲外部變量:結(jié)構(gòu)體內(nèi)部的成員應(yīng)當(dāng)使用什么類型呢,是T、&T還是&mut T呢?

  • 第二個(gè)問(wèn)題關(guān)心的是閉包如何使用外部變量:函數(shù)調(diào)用的self應(yīng)當(dāng)使用什么類型呢,是self&self還是&mut self呢?

如何捕獲外部變量

對(duì)于閉包如何捕獲外部變量,編譯器的原則是“按需捕獲”:在保證能編譯通過(guò)的情況下,結(jié)構(gòu)體內(nèi)部的成員優(yōu)先選擇&T,其次是&mut T,最后選擇T。這個(gè)原則的核心就是“選擇對(duì)結(jié)構(gòu)體外部影響最小的存儲(chǔ)類型”,具體來(lái)說(shuō)就是:

  • 如果一個(gè)外部變量在閉包中只通過(guò)借用指針&使用,那么這個(gè)變量就可使用&捕獲;

  • 如果一個(gè)外部變量在閉包中通過(guò)可變借用指針&mut使用,那么這個(gè)變量就需要使用&mut捕獲;

  • 如果一個(gè)外部變量在閉包中通過(guò)所有權(quán)轉(zhuǎn)移方式使用過(guò),那么這個(gè)變量就需要使用T捕獲;

看下面這個(gè)例子:

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,所以編譯器會(huì)自動(dòng)生成類似于下面這樣的結(jié)構(gòu)體:

struct Closure {
    x: T,
    y: &T,
    z: &mut T,
}

需要注意的是,如果承載閉包的變量不再是局部變量,而是被傳遞出了當(dāng)前作用域,則該閉包必須選擇傳遞所有權(quán)的方式才能保證編譯通過(guò),這時(shí)可以使用move關(guān)鍵字修飾閉包,強(qiáng)制將閉包中變量的捕獲全部使用所有權(quán)轉(zhuǎn)移的方式。例如:

fn make_adder(x: i32) -> Box<Fn(i32) -> i32> {
    Box::new(move |y| x + y)
}

可以看到,局部變量x被傳遞出了函數(shù)外,如果不加move關(guān)鍵字,編譯器會(huì)提示:

error[E0373]: closure may outlive the current function, but it borrows `x`, which is owned by the current function
如何使用外部變量

閉包使用外部變量的方式將影響相應(yīng)的結(jié)構(gòu)體成員函數(shù)的第一個(gè)參數(shù)self的類型,對(duì)于self、&self&mut self三種類型,Rust提供了三個(gè)trait對(duì)其進(jìn)行抽象。這三個(gè)trait是Fn、FnMutFnOnce,它們的定義如下:

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

可以看到,這三個(gè)trait的區(qū)別就在于self的類型不同:

  • FnOnce的調(diào)用參數(shù)為self,表示閉包通過(guò)轉(zhuǎn)移所有權(quán)的方式來(lái)使用外部環(huán)境中的變量,所以該閉包只能調(diào)用一次;

  • FnMut的調(diào)用參數(shù)是&mut self,表示閉包通過(guò)可變借用的方式來(lái)使用外部環(huán)境中的變量;

  • Fn的調(diào)用參數(shù)是&self,表示閉包通過(guò)不可變借用的方式來(lái)使用外部環(huán)境中的變量;

需要注意的是,Fn繼承自FnMut,FnMut繼承自FnOnce,這意味著,如果要實(shí)現(xiàn)Fn,就必須實(shí)現(xiàn)FnMutFnOnce;如果要實(shí)現(xiàn)FnMut,就必須實(shí)現(xiàn)FnOnce。這里面蘊(yùn)含的邏輯是,如果該閉包能夠以Fn方式調(diào)用,那么它也一定能以FnMutFnOnce方式調(diào)用。

看下面這個(gè)例子:

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關(guān)鍵字,它的捕獲方式是&T,使用方式是Fn,這里為了演示,強(qiáng)制使用move關(guān)鍵字將捕獲方式改為T,但由于閉包中對(duì)變量s的使用仍然是只讀,所以使用方式仍然是Fn,而不是FnOnce;閉包use_by_mut是可變方式使用了外部變量s,它的捕獲方式是&mut T,使用方式是FnMut;閉包use_bu_move是所有權(quán)轉(zhuǎn)移方式使用了外部變量s,它的捕獲方式是T,使用方式是FnOnce,所以在第二次調(diào)用use_by_move時(shí)會(huì)報(bào)錯(cuò)。

閉包的使用

閉包一定會(huì)實(shí)現(xiàn)Fn、FnMutFnOnce三種trait之一,所以可以將閉包作為函數(shù)參數(shù)和返回類型,但需要注意的是,不要忘記trait是一種DST類型,它的大小在編譯階段是不固定的,從而不能直接作為參數(shù)類型或者返回值類型,這也是Rust中的trait和其他語(yǔ)言中的接口的重大區(qū)別之一。請(qǐng)看下面的例子,在函數(shù)tset中不可以直接使用Bird作為類型,編譯器會(huì)報(bào)錯(cuò)。

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) {}

難道我們?cè)赗ust中就不能擁有多態(tài)了嗎?那是不可能的,我們有兩種選擇:

  • 靜態(tài)分派:通過(guò)泛型的方式,為不同的泛型類型參數(shù)生成不同版本的函數(shù),實(shí)現(xiàn)編譯期靜態(tài)分派。

fn test<T: Bird>(arg: T) {
    arg.fly();
}
  • 動(dòng)態(tài)分派,通過(guò)trait objetc的方式,將閉包裝箱進(jìn)入堆內(nèi)存中,函數(shù)傳遞的是一個(gè)胖指針,從實(shí)現(xiàn)運(yùn)行期動(dòng)態(tài)分派。

fn test(arg: Box<dyn Bird>) {
    arg.fly();
}

下面我們來(lái)具體介紹一下靜態(tài)分派和動(dòng)態(tài)分派。

靜態(tài)分派

對(duì)于閉包,其泛型參數(shù)的寫法有一些特殊之處,如下面代碼所示:

fn call_with_closure<F>(closure: F) -> i32
where
    F: Fn(i32) -> i32,
{
    closure(1)
}

其中泛型參數(shù)F的約束條件是F: Fn(i32) -> i32,這使得看起來(lái)和普通函數(shù)類型更相似從而更易閱讀。

使用泛型的方式在函數(shù)參數(shù)中可以正常使用,但卻無(wú)法將一個(gè)閉包作為函數(shù)值返回,因?yàn)镽ust中只支持函數(shù)返回具體類型,而閉包是一個(gè)匿名類型,這使得編譯器無(wú)法自動(dòng)推斷且程序員也無(wú)法手動(dòng)指定。這時(shí),可以使用impl trait語(yǔ)法糖,看下面的例子:

fn multiply(m: i32) -> impl Fn(i32) -> i32 {
    move |x| x * m
}

這里的impl Fn(i32) -> i32表示,這個(gè)返回類型,雖然我們不知道它的具體名字,但是知道它滿足Fn(i32) -> i32這個(gè)trait的約束。這個(gè)功能目前還不是特別穩(wěn)定,建議不要激進(jìn)使用,推薦使用下面介紹的動(dòng)態(tài)分派的方式來(lái)解決返回值的問(wèn)題。

動(dòng)態(tài)分派

動(dòng)態(tài)分派是通過(guò)指針的方式來(lái)實(shí)現(xiàn)多態(tài),雖然trait是的DST,但指向trait的指針不是DST。如果我們把trait隱藏到指針的后面,那么就稱它是一個(gè)trait object,而trait object是可以作為參數(shù)和返回類型的。

指向trait的指針就是trait object。假如Bird是一個(gè)trait的名稱,那么dyn Bird就是一個(gè)DST動(dòng)態(tài)大小類型,則&dyn BirdBox<dyn Bird>以及Rc<dyn Bird>等都是trait object。當(dāng)指針指向trait的時(shí)候,它就變成一個(gè)胖指針了,比如Box<dyn Bird>可以理解為:

pub struct TraitObject {
    pub data: *mut(),
    pub vtable: *mut(),
}

它里面包含了兩個(gè)指針,第一個(gè)表示地址,第二個(gè)指向“虛函數(shù)表”,里面有我們需要調(diào)用的具體函數(shù)的地址。這和C++中的虛函數(shù)表內(nèi)存布局有所不同。在C++中,如果一個(gè)類型里面有虛函數(shù),則每一個(gè)這種類型的變量?jī)?nèi)部都包含一個(gè)指向虛函數(shù)表的地址。而在Rust中,對(duì)象本身不包含指向虛函數(shù)表的指針,這個(gè)指針是存在于trait object指針里面的,如果一個(gè)類型實(shí)現(xiàn)了多個(gè)trait,那么不同的trait object指向的虛函數(shù)表也不一樣。

需要注意的是,并不是所有的trait都可以構(gòu)造trait object,trait objetc的構(gòu)造是受到許多約束的。滿足以下條件的trait不能構(gòu)造trait object:

  • 當(dāng)trait有Self: Sized約束時(shí)

  • 當(dāng)函數(shù)除了第一個(gè)參數(shù)外,有Self類型作為參數(shù)或返回類型時(shí)

  • 當(dāng)函數(shù)的第一個(gè)參數(shù)不是Self時(shí)

  • 當(dāng)函數(shù)有泛型參數(shù)時(shí)

對(duì)于后三種情況,如果想把不滿足條件的函數(shù)剔除在外,可以為該函數(shù)加上Self: Sized約束,例如fn foo(&self) where Self: Sized

看完上述內(nèi)容,你們對(duì)Rust中怎么實(shí)現(xiàn)閉包有進(jìn)一步的了解嗎?如果還想了解更多知識(shí)或者相關(guān)內(nèi)容,請(qǐng)關(guān)注創(chuàng)新互聯(lián)行業(yè)資訊頻道,感謝大家的支持。

當(dāng)前名稱:Rust中怎么實(shí)現(xiàn)閉包
網(wǎng)頁(yè)鏈接:http://www.muchs.cn/article42/ihpshc.html

成都網(wǎng)站建設(shè)公司_創(chuàng)新互聯(lián),為您提供關(guān)鍵詞優(yōu)化、外貿(mào)建站網(wǎng)站改版App開(kāi)發(fā)、外貿(mào)網(wǎng)站建設(shè)虛擬主機(jī)

廣告

聲明:本網(wǎng)站發(fā)布的內(nèi)容(圖片、視頻和文字)以用戶投稿、用戶轉(zhuǎn)載內(nèi)容為主,如果涉及侵權(quán)請(qǐng)盡快告知,我們將會(huì)在第一時(shí)間刪除。文章觀點(diǎn)不代表本網(wǎng)站立場(chǎng),如需處理請(qǐng)聯(lián)系客服。電話:028-86922220;郵箱:631063699@qq.com。內(nèi)容未經(jīng)允許不得轉(zhuǎn)載,或轉(zhuǎn)載時(shí)需注明來(lái)源: 創(chuàng)新互聯(lián)

成都定制網(wǎng)站網(wǎng)頁(yè)設(shè)計(jì)