Rust所有權規則概述
Rust的所有權(ownership)規則貫穿整個語言,幾乎每行代碼都涉及到所有權規則,因此需要對所有權規則非常熟悉才能更好地使用Rust。
Rust所有權規則可以總結為如下幾句話:
- Rust中的每個值都有一個被稱為其所有者的變量(即:值的所有者是某個變量)
- 值在任一時刻有且只有一個所有者
- 當所有者(變量)離開作用域,這個值將被銷燬
這裡對第三點做一些補充性的解釋,所有者離開作用域會導致值被銷燬,這個過程實際上是調用一個名為drop的函數來銷燬數據釋放內存。在前文解釋作用域規則時曾提到過,銷燬的數據特指堆棧中的數據,如果變量綁定的值是全局內存區內的數據,則數據不會被銷燬。
例如:
fn main(){ { let mut s = String::from("hello"); } // 跳出作用域,棧中的變量s將被銷燬,其指向的堆 // 中數據也被銷燬,但全局內存區的字符串字面量仍被保留 }
誰是誰的所有者
Rust中每個值都有一個所有者,但這個說法比較容易產生誤會。
例如:
#![allow(unused)] fn main() { let s = String::from("hello"); }
多數人可能會誤以為變量s是堆中字符串數據hello
的所有者,但實際上不是。
前面介紹內存的文章中解釋過,String字符串的實際數據在堆中,但是String大小不確定,所以在棧中使用一個胖指針結構來表示這個String類型的數據,這個胖指針中的指針指向堆中的String實際數據。也就是說,變量s的值是那個胖指針,而不是堆中的實際數據。
因此,變量s是那個胖指針的所有者,而不是堆中實際數據的所有者。
但是,由於胖指針是指向堆中數據的,多數時候為了簡化理解簡化描述方式,也經常會說s是那個堆中實際數據的所有者。但無論如何描述,需要理解所有者和值之間的真相。
Rust中數據的移動
在其他語言中,有深拷貝和淺拷貝的概念,淺拷貝描述的是隻拷貝數據對象的引用,深拷貝描述的是根據引用遞歸到最終的數據並拷貝數據。
在Rust中沒有深淺拷貝的概念,但有移動(move)、拷貝(copy)和克隆(clone)的概念。
看下面的賦值操作,在其他語言中這樣賦值是正確的,但在Rust中這樣的賦值會報錯。
fn main(){ let s1 = String::from("hello"); let s2 = s1; // 將報錯error: borrow of moved value: `s1` println!("{},{}", s1, s2); }
上面的示例中,變量s1綁定了String字符串數據(再次提醒,String數據是胖指針結構而不是指堆中的那些實際數據),此時該數據的所有者是s1。
當執行let s2 = s1;
時,將不會拷貝堆中數據賦值給s2,也不會像其他語言一樣讓變量s2也綁定堆中數據(即,不會拷貝堆數據的引用賦值給s2)。
因此,下圖的內存引用方式不適用於Rust。
如果Rust採用這種內存引用方式,按照Rust的所有權規則,變量在跳出作用域後就銷燬堆中對應數據,那麼在s1和s2離開作用域時會導致二次釋放同一段堆內存,這會導致內存汙染。
Rust採用非常直接的方式,當執行let s2 = s1;
時,直接讓s1無效(s1仍然存在,只是變成未初始化變量,Rust不允許使用未初始化變量,可重新為其賦值),而是隻讓s2綁定堆內存的數據。也就是將s1移動到s2,也稱為值的所有權從s1移給s2。
如圖:
所有權移動後修改數據
定義變量的時候,加上mut
表示變量可修改。當發生所有權轉移時,後擁有所有權的變量也可以加上mut
。
#![allow(unused)] fn main() { let mut x = String::from("hello"); // x將所有權轉移給y,但y無法修改字符串 let y = x; // y.push('C'); // 本行報錯 let a = String::from("hello"); // 雖然a無法修改字符串,但轉移所有權後,b可修改字符串 let mut b = a; b.push('C'); // 本行不報錯 }
移動真的只是移動嗎?
比如下面的示例:
#![allow(unused)] fn main() { let s1 = String::from("hello"); let s2 = s1; }
上面已經分析過,值的所有權會從變量s1轉移到變量s2,所有權的轉移,涉及到的過程是拷貝到目標變量,同時重置原變量到未初始狀態,整個過程就像是進行了一次數據的移動。但注意,上面示例中拷貝的是棧中的胖指針,而不是拷貝堆中的實際數據,因此這樣的拷貝效率是相對較高的。
所有權轉移之後,將只有新的所有者才會指向堆中的實際數據,而原變量將不再指向堆中實際數據,因此所有權轉移之後仍然只有一個指針指向堆中數據。
Move不僅發生在變量賦值過程中,在函數傳參、函數返回數據時也會Move,因此,如果將一個大對象(例如包含很多數據的數組,包含很多字段的struct)作為參數傳遞給函數,是否會讓效率很低下?
按照上面的結論來說,確實如此。但Rust編譯器會對Move語義的行為做出一些優化,簡單來說,當數據量較大且不會引起程序正確性問題時,它會傳遞大對象的指針而非內存拷貝。
此外,對於胖指針類型的變量(如Vec、String),即使發生了拷貝,其性能也不差,因為拷貝的只是它的胖指針部分。
總之,Move雖然發生了內存拷貝,但它的性能並不會太受影響。
此處部分結論參考:https://stackoverflow.com/questions/30288782/what-are-move-semantics-in-rust。
Copy語義
默認情況下,在將一個值保存到某個位置時總是進行值的移動(實際上是拷貝),使得只有目標位置才擁有這個值,而原始變量將變回未初始化狀態,也就是暫時不可用的狀態。這是Rust的移動語義。
Rust還有Copy語義,和Move語義幾乎相同,唯一的區別是Copy後,原始變量仍然可用。
前面說過,Move實際上是進行了拷貝,只不過拷貝後讓原始變量變回未初始化狀態了,而Copy的行為,就是保留原始變量。
但Rust默認是使用Move語義,如果想要使用Copy語義,要求要拷貝的數據類型實現了Copy Trait。
例如,i32默認就已經實現了Copy Trait,因此它在進行所有權轉移的時候,會自動使用Copy語義,而不是Move語義。
#![allow(unused)] fn main() { let x = 3; // 3是原始數據類型,它直接存儲在棧中,所以x變量的值是3,x擁有3 let n = x; // Copy x的值(即3)到變量n,n現在擁有一個3,但x仍然擁有自己的3 }
Rust中默認實現了Copy Trait的類型,包括但不限於:
- 所有整數類型,比如u32
- 所有浮點數類型,比如f64
- 布爾類型,bool,它的值是true和false
- 字符類型,char
- 元組,當且僅當其包含的類型也都是Copy的時候。比如
(i32, i32)
是Copy的,但(i32, String)
不是 - 共享指針類型或共享引用類型
對於那些沒有實現Copy的自定義類型,可以手動去實現Copy(要求同時實現Clone),方式很簡單:
#![allow(unused)] fn main() { #[derive(Copy, Clone)] struct Abc(i32, i32); }
下面是實現了Copy和未實現Copy時的一個對比示例:
#[derive(Debug)] struct Xyz(i32, i32); #[derive(Copy, Clone, Debug)] struct Def(i32, i32); fn main() { let x = Xyz(11, 22); let y = x; // println!("x: {}", x); // 報錯 println!("y: {:?}", y); let d = Def(33, 44); let e = d; println!("d: {:?}", d); println!("e: {:?}", e); }
克隆數據
雖然實現Copy Trait可以讓原變量繼續擁有自己的值,但在某些需求下,不便甚至不能去實現Copy。這時如果想要繼續使用原變量,可以使用clone()
方法手動拷貝變量的數據,同時不會讓原始變量變回未初始化狀態。
fn main(){ let s1 = String::from("hello"); // 克隆s1,克隆之後,變量s1仍然綁定原始數據 let s2 = s1.clone(); println!("{},{}", s1, s2); }
但不是所有數據類型都可以進行克隆,只有那些實現了Clone Trait的類型才可以進行克隆(Trait類似於面嚮對象語言中的接口,如果不瞭解可先不管Trait是什麼),常見的數據類型都已經實現了Clone,因此它們可以直接使用clone()來克隆。
對於那些沒有實現Clone Trait的自定義類型,需要手動實現Clone Trait。在自定義類型之前加上#[derive(Copy, Clone)]
即可。例如:
#![allow(unused)] fn main() { #[derive(Copy, Clone)] struct Abc(i32, i32); }
這樣Abc類型的值就可以使用clone()方法進行克隆。
要注意Copy和Clone時的區別,如果不考慮自己實現Copy trait和Clone trait,而是使用它們的默認實現,那麼:
- Copy時,只拷貝變量本身的值,如果這個變量指向了其它數據,則不會拷貝其指向的數據
- Clone時,拷貝變量本身的值,如果這個變量指向了其它數據,則也會拷貝其指向的數據
也就是說,Copy是淺拷貝,Clone是深拷貝,Rust會對每個字段每個元素遞歸調用clone(),直到最底部。
例如:
fn main() { let vb0 = vec!["s1".to_string()]; let v = vec![vb0]; println!("{:p}", &v[0][0]); // 0x21c43a20c50 let vc = v.clone(); println!("{:p}", &vc[0][0]); // 0x21c43a20b70 }
所以,使用Clone的默認實現時,clone()操作的性能是較低的。但可以自己實現自己的克隆邏輯,也不一定總是會效率低。比如Rc,它的clone用於增加引用計數,同時只拷貝少量數據,它的clone效率並不低。
函數參數和返回值的所有權移動
函數參數類似於變量賦值,在調用函數時,會將所有權移動給函數參數。
函數返回時,返回值的所有權從函數內移動到函數外變量。
例如:
fn main(){ let s1 = String::from("hello"); // 所有權從s1移動到f1的參數 // 然後f1返回值的所有權移動給s2 let s2 = f1(s1); // 注意,println!()不會轉移參數s2的所有權 println!("{}", s2); let x = 4; f2(x); // 沒有移動所有權,而是拷貝一份給f2參數 } // 首先x跳出作用域, // 然後s2跳出作用域,並釋放對應堆內存數據, // 最後s1跳出作用域,s1沒有所有權,所以沒有任何其他影響 fn f1(s: String) -> String { let ss = String::from("world"); println!("{},{}", s,ss); s // 返回值s的所有權移動到函數外 } // ss跳出作用域 fn f2(i: i32){ println!("{}",i); } // i跳出作用域
很多時候,變量傳參之後丟失所有權是非常不方便的,這意味著函數調用之後,原變量就不可用了。為了解決這個問題,可以將變量的引用傳遞給參數。引用是保存在棧中的,它實現了Copy Trait,因此在傳遞引用時,所有權轉移的過程實際上是拷貝了引用,這樣不會丟失原變量的所有權,效率也更高。