rust中move捆等、copy滞造、clone、drop和閉包捕獲

rust中move栋烤、copy谒养、clone、drop和閉包捕獲

本文中的變量明郭,指的是通過如下代碼定義的常量a和變量b买窟。
實例指的是綁定到a的i32類型在stack內(nèi)存的數(shù)據(jù)丰泊,和綁定到b變量的String類型在stack內(nèi)存和heap內(nèi)存中的數(shù)據(jù)。

let a = 0_u32;
let mut b = "Hello".to_string();

先說說使用場景

  • move始绍、copy的應(yīng)用場景瞳购,主要是在變量賦值、函數(shù)調(diào)用的傳入?yún)?shù)亏推、函數(shù)返回值学赛、閉包的變量捕獲。

  • clone需要顯式調(diào)用吞杭。

  • drop是在變量的作用范圍結(jié)束時盏浇,被自動調(diào)用。

  • 閉包中使用了外部變量芽狗,就會有閉包捕獲缠捌。

move語義

rust中的類型,如果沒有實現(xiàn)Copy trait译蒂,那么在此類型的變量賦值、函數(shù)入?yún)⒁耆础⒑瘮?shù)返回值都是move語義柔昼。這是與c++的最大區(qū)別,從c++11開始炎辨,右值引用的出現(xiàn)捕透,才有了move語義。但rust天生就是move語義碴萧。

如下的代碼中乙嘀,變量a綁定的String實例,被move給了b變量破喻,此后a變量就是不可訪問了(編譯器會幫助我們檢查)虎谢。然后b變量綁定的String實例又被move到了f1函數(shù)中,曹质,b變量就不可訪問了婴噩。f1函數(shù)對傳入的參數(shù)做了一定的運算后,再將運算結(jié)果返回羽德,這是函數(shù)f1的返回值被move到了c變量几莽。在代碼結(jié)尾時,只有c變量是有效的宅静。

fn f1(s: String) -> String {
    s + " world!"
}

let a = String::from("Hello");
let b = a;
let c = f1(b);

注意章蚣,如上的代碼中,String類型沒有實現(xiàn)Copy trait姨夹,所以在變量傳遞的過程中纤垂,都是move語義矾策。

copy語義

rust中的類型,如果實現(xiàn)了Copy trait洒忧,那么在此類型的變量賦值蝴韭、函數(shù)入?yún)ⅰ⒑瘮?shù)返回值都是copy語義熙侍。這也是c++中默認的變量傳遞語義榄鉴。

看看類似的代碼,變量a綁定的i32實例蛉抓,被copy給了b變量庆尘,此后a、b變量同時有效巷送,并且是兩個不同的實例驶忌。然后a變量綁定的i32實例又被copy到了f1函數(shù)中,a變量仍然有效笑跛。傳入f1函數(shù)的參數(shù)i是一個新的實例付魔,做了一定的運算后,再將運算結(jié)果返回飞蹂。這時函數(shù)f1的返回值被copy到了c變量几苍,同時f1函數(shù)中的運算結(jié)果作為臨時變量也被銷毀(不會調(diào)用drop,如果類型實現(xiàn)了Copy trait陈哑,就不能有Drop trait)妻坝。傳入b變量調(diào)用f1的過程是相同的,只是返回值被copy給了d變量惊窖。在代碼結(jié)尾時刽宪,a、b界酒、c圣拄、d變量都是有效的。

fn f2(i: i32) -> i32 {
    i + 10
}

let a = 1_i32;
let b = a;
let c = f1(a);
let d = f1(b);

這里再強調(diào)下毁欣,i32類型實現(xiàn)了Copy trait售担,所以整個變量傳遞過程,都是copy語義署辉。

clone語義

move和copy語義都是隱式的族铆,clone需要顯式的調(diào)用。

參考類似的代碼哭尝,變量a綁定的String實例哥攘,在賦值前先clone了一個新的實例,然后將新實例move給了b變量,此后a逝淹、b變量同時有效耕姊。然后b變量在傳入f1函數(shù)前,又clone一個新實例栅葡,再將這個新實例move到f1函數(shù)中茉兰。f1函數(shù)對傳入的參數(shù)做了一定的運算后,再將運算結(jié)果返回欣簇,這里函數(shù)f1的返回值被move到了c變量规脸。在代碼結(jié)尾時,a熊咽、b莫鸭、c變量都是有效的。

fn f1(s: String) -> String {
    s + " world!"
}

let a = String::from("Hello");
let b = a.clone();
let c = f1(b.clone());

在這個過程中横殴,在隱式move前被因,變量clone出新實例并將新實例move出去,變量本身保持不變衫仑。

drop語義

rust的類型可以實現(xiàn)Drop trait梨与,也可以不實現(xiàn)Drop trait。但是對于實現(xiàn)了Copy trait的類型文狱,不能實現(xiàn)Drop trait粥鞋。也就是說CopyDrop兩個trait對同一個類型只能有一個,魚與熊掌不可兼得如贷。

變量在離開作用范圍時,編譯器會自動銷毀變量到踏,如果變量類型有Drop trait杠袱,就先調(diào)用Drop::drop方法,做資源清理窝稿,一般會回收heap內(nèi)存等資源楣富,然后再收回變量所占用的stack內(nèi)存。如果變量沒有Drop trait伴榔,那就只收回stack內(nèi)存纹蝴。

正是由于在Drop::drop方法會做資源清理,所以CopyDrop trait只能二選一踪少。如果類型實現(xiàn)了Copy trait塘安,在copy語義中并不會調(diào)用Clone::clone方法,不會做deep copy援奢,那就會出現(xiàn)兩個變量同時擁有一個資源(比如說是heap內(nèi)存等)兼犯,在這兩個變量離開作用范圍時,會分別調(diào)用Drop::drop方法釋放資源,這就會出現(xiàn)double free錯誤切黔。

copy與clone語義區(qū)別

先看看兩者的定義:

pub trait Clone: Sized {
    fn clone(&self) -> Self;

    fn clone_from(&mut self, source: &Self) {
        *self = source.clone()
    }
}

pub trait Copy: Clone {
    // Empty.
}

CloneCopy的super trait砸脊,一個類型要實現(xiàn)Copy就必須先實現(xiàn)Clone

再留意看纬霞,Copy trait中沒有任何方法凌埂,所以在copy語義中不可以調(diào)用用戶自定義的資源復(fù)制代碼,也就是不可以做deep copy诗芜。copy語義就是變量在stack內(nèi)存的按位復(fù)制瞳抓,沒有其他任何多余的操作。

Clone中有clone方法绢陌,用戶可以對類型做自定義的資源復(fù)制挨下,這就可以做deep copy。在clone語義中脐湾,類型的Clone::clone方法會被調(diào)用臭笆,程序員在Clone::clone方法中做資源復(fù)制,同時在Clone::clone方法返回時秤掌,變量的stack內(nèi)存也會被按照位復(fù)制一份愁铺,生成一個完整的新實例。

自定義類型實現(xiàn)CopyClone trait

Clone trait闻鉴,對于任何自定義類型都可以實現(xiàn)茵乱。Copy trait只有自定義類型中的field全部實現(xiàn)了Copy trait,才可以實現(xiàn)Copy trait孟岛。

如下代碼舉例瓶竭,struct S1中的field分別是i32usize類型,都是有Copy trait渠羞,所以S1可以實現(xiàn)Copy trait斤贰。你可以通過#[derive(Copy, Clone)]方式實現(xiàn),也可以自己寫代碼實現(xiàn)次询。

struct S1 {
    i: i32,
    u: usize,
}
impl Copy for S1 {}
impl Clone for S1 {
    fn clone(&self) -> Self {
        // 此處是S1的copy語義調(diào)用荧恍。
        // 正是i32和usize的Copy trait,才有了S1的Copy trait屯吊。
        *self   
    }
}

但是對于如下的struct S2送巡,由于S2的field中有String類型,String類型沒有實現(xiàn)Copy trait盒卸,所以S2類型就不能實現(xiàn)Copy trait骗爆。S2中也包含了E1類型,E1類型沒有實現(xiàn)CloneCopy trait蔽介,但是我們可以自己實現(xiàn)S2類型的Clone trait淮腾,在Clone::clone方法中生成新的E1實例糟需,這就可以clone出新的S2實例。

enum E1 {
    Text,
    Digit,
}
struct S2 {
    u: usize,
    e: E1,
    s: String,
}
impl Clone for S2 {
    fn clone(&self) -> Self {
        // 生成新的E1實例
        let e = match self.e {
            E1::Text => E1::Text,
            E1::Digit => E1::Digit,
        };
        Self {
            u: self.u,
            e,
            s: self.s.clone(),
        }
    }
}

注意谷朝,在這種情況下洲押,不能通過#[derive(Clone)]自動實現(xiàn)S2類型的Clone trait。只有類型中的所有field都有Clone圆凰,才可以通過#[derive(Clone)]自動實現(xiàn)Clone trait杈帐。

閉包捕獲變量

與閉包關(guān)聯(lián)的是三個trait的定義,分別是FnOnce专钉、FnMutFn挑童,定義如下:

pub trait FnOnce<Args> {
    type Output;
    fn call_once(self, args: Args) -> Self::Output;
}

pub trait FnMut<Args>: FnOnce<Args> {
    fn call_mut(&mut self, args: Args) -> Self::Output;
}

pub trait Fn<Args>: FnMut<Args> {
    fn call(&self, args: Args) -> Self::Output;
}

注意三個trait中方法的receiver參數(shù),FnOnceself參數(shù)跃须,FnMut&mut self參數(shù)站叼,Fn&self參數(shù)。

原則說明如下:

  • 如果閉包只是對捕獲變量的非修改操作菇民,閉包捕獲的是&T類型尽楔,閉包按照Fn trait方式執(zhí)行,閉包可以重復(fù)多次執(zhí)行第练。

  • 如果閉包對捕獲變量有修改操作阔馋,閉包捕獲的是&mut T類型,閉包按照FnMut trait方式執(zhí)行娇掏,閉包可以重復(fù)多次執(zhí)行呕寝。

  • 如果閉包會消耗掉捕獲的變量,變量被move進閉包婴梧,閉包按照FnOnce trait方式執(zhí)行下梢,閉包只能執(zhí)行一次。

對于實現(xiàn)Copy trait和沒有實現(xiàn)Copy trait對類型塞蹭,具體參考如下對代碼說明孽江。

類型實現(xiàn)了Copy,閉包中是&T操作

如下的代碼浮还,f閉包對i變量竟坛,沒有修改操作闽巩,此處捕獲到的是&i钧舌,所以f就是按照Fn trait方式執(zhí)行,可以多次執(zhí)行f涎跨。

fn test_fn_i8() {
    let mut i = 1_i8;
    let f = || i + 1;

    // f閉包對i是immutable borrowed洼冻,是Fn trait
    let v = f();

    // f閉包中只是immutable borrowed,此處可以再做borrowed隅很。
    dbg!(&i);   

    // f可以調(diào)用多次
    let v2 = f();

    // 此時撞牢,f閉包生命周期已經(jīng)結(jié)束,i已經(jīng)沒有borrowed了,所以此處可以mutable borrowed屋彪。
    i += 10;

    assert_eq!(2, v);
    assert_eq!(2, v2);
    assert_eq!(11, i);
}

類型實現(xiàn)了Copy所宰,閉包中是&mut T操作

如下的代碼,f閉包對i變量畜挥,有修改操作仔粥,此處捕獲到的是&mut i,所以f就是按照FnMut trait方式執(zhí)行蟹但,注意f本身也是mut躯泰,可以多次執(zhí)行f。

fn test_fn_mut_i8() {
    let mut i = 1_i8;
    let mut f = || {
        i += 1;
        i
    };

    // f閉包對i是mutable borrowed华糖,是FnMut trait
    let v = f();

    // i已經(jīng)被mutable borrowed麦向,就不能再borrowed了。
    // dbg!(&i);   

    // f可以調(diào)用多次
    let v2 = f();

    // 此時客叉,f閉包生命周期已經(jīng)結(jié)束诵竭,i沒有mutable borrowed了,所以此處可以mutable borrowed十办。
    i += 10;    

    assert_eq!(2, v);
    assert_eq!(3, v2);
    assert_eq!(13, i);
}

類型實現(xiàn)了Copy秀撇,閉包使用move關(guān)鍵字,閉包中是&mut T操作

如下的代碼向族,f閉包對i變量呵燕,有修改操作,并且使用了move關(guān)鍵字件相。由于i8實現(xiàn)了Copy trait再扭,此處i會copy一個新實例,并將新實例move到閉包中夜矗,在閉包中的實際是一個新的i8變量泛范。f就是按照FnMut trait方式執(zhí)行,注意f本身也是mut紊撕,可以多次執(zhí)行f罢荡。

重點說明,此處move關(guān)鍵字的使用对扶,強制copy一個新的變量区赵,將新變量move進閉包。

fn test_fn_mut_i8_move() {
    let mut i = 1_i8;
    let mut f = move || {
        i += 1;
        i
    };

    // i8有Copy trait浪南,f閉包中是move進去的新實例笼才,新實例不會被消耗,是FnMut trait
    let v = f();

    // i8有Copy trait络凿,f閉包中是move進去的新實例骡送,i沒有borrowed昂羡,所以此處可以mutable borrowed。
    i += 10;    

    // f可以調(diào)用多次
    let v2 = f();

    assert_eq!(2, v);
    assert_eq!(3, v2);
    assert_eq!(11, i);
}

類型沒有實現(xiàn)Copy摔踱,閉包中是&T操作

如下的代碼虐先,f閉包對s變量,沒有修改操作派敷,此處捕獲到的是&s赴穗,f按照Fn trait方式執(zhí)行,可以多次執(zhí)行f膀息。

fn test_fn_string() {
    let mut s = "Hello".to_owned();
    let f = || -> String {
        dbg!(&s);
        "world".to_owned()
    };

    // f閉包對s是immutable borrowed般眉,是Fn trait
    let v = f();

    // f閉包中是immutable borrowed,此處是第二個immutable borrowed潜支。
    dbg!(&s);

    // f可以調(diào)用多次
    let v2 = f();

    // f閉包生命周期結(jié)束甸赃,s已經(jīng)沒有borrowed,所以此處可以mutable borrowed
    s += " moto";

    assert_eq!("world", &v);
    assert_eq!("world", &v2);
    assert_eq!("Hello moto", &s);
}

類型沒有實現(xiàn)Copy冗酿,閉包中是&mut T操作

如下的代碼埠对,f閉包對s變量,調(diào)用push_str(&mut self, &str)方法修改裁替,此處捕獲到的是&mut s项玛,f是按照FnMut trait方式執(zhí)行,注意f本身是mut弱判,f可以多次執(zhí)行f襟沮。

fn test_fn_mut_string() {
    let mut s = "Hello".to_owned();
    let mut f = || -> String {
        s.push_str(" world");
        s.clone()
    };

    // f閉包對s是mutable borrowed,是FnMut trait
    let v = f();

    // s是mutable borrowed昌腰,此處不能再borrowed开伏。
    // dbg!(&s);

    // f可以多次調(diào)用
    let v2 = f();

    // f閉包生命周期結(jié)束,s已經(jīng)沒有borrowed遭商,所以此處可以mutable borrowed
    s += " moto";

    assert_eq!("Hello world", &v);
    assert_eq!("Hello world world", &v2);
    assert_eq!("Hello world world moto", &s);
}

類型沒有實現(xiàn)Copy固灵,閉包使用move關(guān)鍵字,閉包中是&mut T操作

如下的代碼劫流,f閉包對s變量巫玻,調(diào)用push_str(&mut self, &str)方法修改,閉包使用move關(guān)鍵字祠汇,s被move進閉包仍秤,s沒有被消耗,f是按照FnMut trait方式執(zhí)行座哩,注意f本身是mut徒扶,f可以多次執(zhí)行粮彤。

fn test_fn_mut_move_string() {
    let mut s = "Hello".to_owned();
    let mut f = move || -> String {
        s.push_str(" world");
        s.clone()
    };

    // s被move進f閉包中根穷,s沒有被消耗姜骡,是FnMut trait
    let v = f();

    // s被move進閉包,s不能被borrowed
    // dbg!(&s);

    // f可以多次調(diào)用
    let v2 = f();

    // s被move進閉包屿良,s不能被borrowed圈澈,但是可以綁定新實例
    s = "moto".to_owned();

    assert_eq!("Hello world", &v);
    assert_eq!("Hello world world", &v2);
    assert_eq!("moto", &s);
}

類型沒有實現(xiàn)Copy,閉包中是&mut T操作尘惧,捕獲的變量被消耗

如下的代碼康栈,f閉包對s變量,調(diào)用push_str(&mut self, &str)方法修改喷橙,s被閉包消耗啥么,此處捕獲到的是s本身,s被move到閉包中贰逾,閉包外部s就不可見了悬荣。f是按照FnOnce trait方式執(zhí)行,不可以多次執(zhí)行f疙剑。

fn test_fn_once_string() {
    let mut s = "Hello".to_owned();
    let f = || -> String {
        s.push_str(" world");
        s   // s被消耗
    };

    // s被move進f閉包中氯迂,s被消耗,是FnOnce trait
    let v = f();

    // s變量已經(jīng)被move了言缤,不能再被borrowed
    // dbg!(&s);
    
    // f只能調(diào)用一次
    // let v2 = f();

    // s被move進閉包嚼蚀,s不能被borrowed,但是可以綁定新實例
    s = "moto".to_owned();

    assert_eq!("Hello world", v);
    assert_eq!("moto", &s);
}

類型沒有實現(xiàn)Copy管挟,閉包使用move關(guān)鍵字轿曙,閉包中是T操作,捕獲的變量被消耗

如下的代碼僻孝,f閉包對s變量拳芙,調(diào)用into_boxed_str(self)方法,s被閉包消耗皮璧,此處捕獲到的是s本身舟扎,s被move到閉包中,閉包外部s就不可見了悴务。f是按照FnOnce trait方式執(zhí)行睹限,不可以多次執(zhí)行f。

本例中move關(guān)鍵字不是必須的讯檐。

fn test_fn_once_move_string() {
    let mut s = "Hello".to_owned();
    let f = move || s.into_boxed_str();

    // s被move進f閉包中羡疗,s被消耗,是FnOnce trait
    let v = f();

    // s變量已經(jīng)被move了别洪,不能再被borrowed
    // dbg!(&s);
    
    // f只能調(diào)用一次
    // let v2 = f();

    // s被move進閉包叨恨,s不能被borrowed,但是可以綁定新實例
    s = "moto".to_owned();

    assert_eq!("Hello", &*v);
    assert_eq!("moto", &s);
}

最后總結(jié)

move挖垛、copy痒钝、clone秉颗、drop和閉包捕獲是rust中基本的概念,代碼過程中隨時要清楚每個變量的變化送矩。這會讓自己的思路更清晰蚕甥,rustc也會變得溫柔馴服。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末栋荸,一起剝皮案震驚了整個濱河市菇怀,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌晌块,老刑警劉巖爱沟,帶你破解...
    沈念sama閱讀 216,470評論 6 501
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異匆背,居然都是意外死亡钥顽,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,393評論 3 392
  • 文/潘曉璐 我一進店門靠汁,熙熙樓的掌柜王于貴愁眉苦臉地迎上來蜂大,“玉大人,你說我怎么就攤上這事蝶怔∧唐郑” “怎么了?”我有些...
    開封第一講書人閱讀 162,577評論 0 353
  • 文/不壞的土叔 我叫張陵踢星,是天一觀的道長澳叉。 經(jīng)常有香客問我,道長沐悦,這世上最難降的妖魔是什么成洗? 我笑而不...
    開封第一講書人閱讀 58,176評論 1 292
  • 正文 為了忘掉前任,我火速辦了婚禮藏否,結(jié)果婚禮上瓶殃,老公的妹妹穿的比我還像新娘。我一直安慰自己副签,他們只是感情好遥椿,可當(dāng)我...
    茶點故事閱讀 67,189評論 6 388
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著淆储,像睡著了一般冠场。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上本砰,一...
    開封第一講書人閱讀 51,155評論 1 299
  • 那天碴裙,我揣著相機與錄音,去河邊找鬼。 笑死舔株,一個胖子當(dāng)著我的面吹牛莺琳,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播督笆,決...
    沈念sama閱讀 40,041評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼诱贿!你這毒婦竟也來了娃肿?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 38,903評論 0 274
  • 序言:老撾萬榮一對情侶失蹤珠十,失蹤者是張志新(化名)和其女友劉穎料扰,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體焙蹭,經(jīng)...
    沈念sama閱讀 45,319評論 1 310
  • 正文 獨居荒郊野嶺守林人離奇死亡晒杈,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,539評論 2 332
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了孔厉。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片拯钻。...
    茶點故事閱讀 39,703評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖撰豺,靈堂內(nèi)的尸體忽然破棺而出粪般,到底是詐尸還是另有隱情,我是刑警寧澤污桦,帶...
    沈念sama閱讀 35,417評論 5 343
  • 正文 年R本政府宣布亩歹,位于F島的核電站,受9級特大地震影響凡橱,放射性物質(zhì)發(fā)生泄漏小作。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 41,013評論 3 325
  • 文/蒙蒙 一稼钩、第九天 我趴在偏房一處隱蔽的房頂上張望顾稀。 院中可真熱鬧,春花似錦坝撑、人聲如沸础拨。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,664評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽诡宗。三九已至,卻和暖如春击儡,著一層夾襖步出監(jiān)牢的瞬間塔沃,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 32,818評論 1 269
  • 我被黑心中介騙來泰國打工阳谍, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留蛀柴,地道東北人螃概。 一個月前我還...
    沈念sama閱讀 47,711評論 2 368
  • 正文 我出身青樓,卻偏偏與公主長得像鸽疾,于是被迫代替她去往敵國和親吊洼。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 44,601評論 2 353