寫C++的時候,指針都在明面上。到了Rust,指針在很多場合都藏了起來。但遺憾的是,它們并不是真的想被遺忘掉,而是在和你躲貓貓,最終你不得不把它們揪出來,游戲才能繼續。
1. 所有者被修改了會發生什么?
先讓下面這段看似沒有指針代碼引出問題:
fn main(){
let mut x = Box::new("ABC");
println!("x is {}", x);
x = Box::new("XYZ");
println!("x is {}", x);
}
x is ABC
x is XYZ
問題可能你也猜到了:
第1行代碼里我們在堆上存放“ABC”的內存,到底泄露了沒有?
如果沒有,是什么時候釋放的?
如何證明?
好吧,還記得《The Rust Programming Language》里的Ownership Rules是這么說的:
- Each value in Rust has a variable that’s called its owner.
- There can only be one owner at a time.
- When the owner goes out of scope, the value will be dropped.
可是此時此刻,即便是權威描述,也難免心生困惑了。
接下來,我們做個實驗,嘗試回答問題。
2. 利用智能指針的Drop trait
證明的思路是這樣:
假設,Rust能正常的進行內存釋放;
已知,
std::boxed::Box
是一個智能指針(結構體);而且,
std::boxed::Box
實現了Drop trait
;那么,實現一個自定義結構體的
Drop trait
;接著,觀察實例的Owner被修改時
std::ops::Drop::drop
被調用的時機點;推理,得出上例中的“ABC”的釋放時機;
完畢;
代碼如下:
#[derive(Debug)]
struct MyPointer{}
?
impl Drop for MyPointer{
fn drop(&mut self) {
println!("Dropping MyPointer!");
}
}
?
fn main(){
let mut x = MyPointer{};
println!("x is {:?}", x);
x = MyPointer{};
println!("Now x is {:?}", x);
}
輸出:
x is MyPointer // 持有的值。
Dropping MyPointer! // Owner被修改時釋放值。
Now x is MyPointer // 新持有的值。
Dropping MyPointer! // 持有到花括號結束時釋放值。
輸出的順序,即是我們想要的答案:
觀察到,drop會在Owner被修改的第一時間被調用;
推理出,字符串“ABC”會在Owner被修改的第一時間被釋放掉;
3. std::boxed::Box真正的實現
順便看下Drop for Box
的源碼,實現居然是空的:
#[stable(feature = "rust1", since = "1.0.0")]
unsafe impl<#[may_dangle] T: ?Sized> Drop for Box<T> {
fn drop(&mut self) {
// FIXME: Do nothing, drop is currently performed by compiler.
}
}
也不難理解,內置在Rust語言中的所有權機制,的確不應該由Drop trait來操心,否則自己實現Drop的程序員還得操心內存釋放的問題,也就太low了。
雖然我們沒有親眼看到Rust釋放內存的底層代碼,但是能看到drop
能在合適的時機點被觸發已經足夠了。
4. 小結
再回頭看Ownership Rules,其實說的還是很精準,可以這么理解:因為當作為Owner的變量被修改后,堆上的值就相當于沒有了Owner(突然消失在作用域中),那值自然也就被釋放了。
Rust的內存回收的確不用操心,高效且精準。
通過這個例子,再次加深了我對Rust一個不同尋常的印象,就是:Rust變量作用域 <= 花括號作用域。無論是借用的生命周期的檢查,還是上例中被修改的所有者,Rust編譯器都會對其作用域盡早的進行判定,而不是等待花括號結束。