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粥鞋。也就是說Copy
和Drop
兩個trait對同一個類型只能有一個,魚與熊掌不可兼得如贷。
變量在離開作用范圍時,編譯器會自動銷毀變量到踏,如果變量類型有Drop
trait杠袱,就先調(diào)用Drop::drop
方法,做資源清理窝稿,一般會回收heap內(nèi)存等資源楣富,然后再收回變量所占用的stack內(nèi)存。如果變量沒有Drop
trait伴榔,那就只收回stack內(nèi)存纹蝴。
正是由于在Drop::drop
方法會做資源清理,所以Copy
和Drop
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.
}
Clone
是Copy
的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)Copy
和Clone
trait
Clone
trait闻鉴,對于任何自定義類型都可以實現(xiàn)茵乱。Copy
trait只有自定義類型中的field全部實現(xiàn)了Copy
trait,才可以實現(xiàn)Copy
trait孟岛。
如下代碼舉例瓶竭,struct S1
中的field分別是i32
和usize
類型,都是有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)Clone
和Copy
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
专钉、FnMut
和Fn
挑童,定義如下:
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ù),FnOnce
是self
參數(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也會變得溫柔馴服。