所有權(Ownership)
在進入正題之前,大家先回憶下一般的編程語言知識。 對於一般的編程語言,通常會先聲明一個變量,然後初始化它。 例如在C語言中:
int* foo() {
int a; // 變量a的作用域開始
a = 100;
char *c = "xyz"; // 變量c的作用域開始
return &a;
} // 變量a和c的作用域結束
儘管可以編譯通過,但這是一段非常糟糕的代碼,現實中我相信大家都不會這麼去寫。變量a和c都是局部變量,函數結束後將局部變量a的地址返回,但局部變量a存在stack中,在離開作用域後,局部變量所申請的stack上內存都會被系統回收,從而造成了Dangling Pointer的問題。這是一個非常典型的內存安全問題。很多編程語言都存在類似這樣的內存安全問題。再來看變量c,c的值是常量字符串,存儲於常量區,可能這個函數我們只調用了一次,我們可能不再想使用這個字符串,但xyz只有當整個程序結束後系統才能回收這片內存,這點讓程序員是不是也很無奈?
備註:對於
xyz,可根據實際情況,通過heap的方式,手動管理(申請和釋放)內存。
所以,內存安全和內存管理通常是程序員眼中的兩大頭疼問題。令人興奮的是,Rust卻不再讓你擔心內存安全問題,也不用再操心內存管理的麻煩,那Rust是如何做到這一點的?請往下看。
綁定(Binding)
重要:首先必須強調下,準確地說Rust中並沒有變量這一概念,而應該稱為標識符,目標資源(內存,存放value)綁定到這個標識符:
#![allow(unused)] fn main() { { let x: i32; // 標識符x, 沒有綁定任何資源 let y: i32 = 100; // 標識符y,綁定資源100 } }
好了,我們繼續看下以下一段Rust代碼:
#![allow(unused)] fn main() { { let a: i32; println!("{}", a); } }
上面定義了一個i32類型的標識符a,如果你直接println!,你會收到一個error報錯:
error: use of possibly uninitialized variable:
a
這是因為Rust並不會像其他語言一樣可以為變量默認初始化值,Rust明確規定變量的初始值必須由程序員自己決定。
正確的做法:
#![allow(unused)] fn main() { { let a: i32; a = 100; //必須初始化a println!("{}", a); } }
其實,let關鍵字並不只是聲明變量的意思,它還有一層特殊且重要的概念-綁定。通俗的講,let關鍵字可以把一個標識符和一段內存區域做“綁定”,綁定後,這段內存就被這個標識符所擁有,這個標識符也成為這段內存的唯一所有者。
所以,a = 100發生了這麼幾個動作,首先在stack內存上分配一個i32的資源,並填充值100,隨後,把這個資源與a做綁定,讓a成為資源的所有者(Owner)。
作用域
像C語言一樣,Rust通過{}大括號定義作用域:
#![allow(unused)] fn main() { { { let a: i32 = 100; } println!("{}", a); } }
編譯後會得到如下error錯誤:
b.rs:3:20: 3:21 error: unresolved name
a[E0425] b.rs:3 println!("{}", a);
像C語言一樣,在局部變量離開作用域後,變量隨即會被銷燬;但不同是,Rust會連同變量綁定的內存,不管是否為常量字符串,連同所有者變量一起被銷燬釋放。所以上面的例子,a銷燬後再次訪問a就會提示無法找到變量a的錯誤。這些所有的一切都是在編譯過程中完成的。
移動語義(move)
先看如下代碼:
#![allow(unused)] fn main() { { let a: String = String::from("xyz"); let b = a; println!("{}", a); } }
編譯後會得到如下的報錯:
c.rs:4:20: 4:21 error: use of moved value:
a[E0382] c.rs:4 println!("{}", a);
錯誤的意思是在println中訪問了被moved的變量a。那為什麼會有這種報錯呢?具體含義是什麼?
在Rust中,和“綁定”概念相輔相成的另一個機制就是“轉移move所有權”,意思是,可以把資源的所有權(ownership)從一個綁定轉移(move)成另一個綁定,這個操作同樣通過let關鍵字完成,和綁定不同的是,=兩邊的左值和右值均為兩個標識符:
#![allow(unused)] fn main() { 語法: let 標識符A = 標識符B; // 把“B”綁定資源的所有權轉移給“A” }
move前後的內存示意如下:
Before move:
a <=> 內存(地址:A,內容:"xyz")
After move:
a
b <=> 內存(地址:A,內容:"xyz")
被move的變量不可以繼續被使用。否則提示錯誤error: use of moved value。
這裡有些人可能會疑問,move後,如果變量A和變量B離開作用域,所對應的內存會不會造成“Double Free”的問題?答案是否定的,Rust規定,只有資源的所有者銷燬後才釋放內存,而無論這個資源是否被多次move,同一時刻只有一個owner,所以該資源的內存也只會被free一次。
通過這個機制,就保證了內存安全。是不是覺得很強大?
Copy特性
有讀者仿照“move”小節中的例子寫了下面一個例子,然後說“a被move後是可以訪問的”:
#![allow(unused)] fn main() { let a: i32 = 100; let b = a; println!("{}", a); }
編譯確實可以通過,輸出為100。這是為什麼呢,是不是跟move小節裡的結論相悖了?
其實不然,這其實是根據變量類型是否實現Copy特性決定的。對於實現Copy特性的變量,在move時會拷貝資源到新內存區域,並把新內存區域的資源binding為b。
Before move:
a <=> 內存(地址:A,內容:100)
After move:
a <=> 內存(地址:A,內容:100)
b <=> 內存(地址:B,內容:100)
move前後的a和b對應資源內存的地址不同。
在Rust中,基本數據類型(Primitive Types)均實現了Copy特性,包括i8, i16, i32, i64, usize, u8, u16, u32, u64, f32, f64, (), bool, char等等。其他支持Copy的數據類型可以參考官方文檔的Copy章節。
淺拷貝與深拷貝
前面例子中move String和i32用法的差異,其實和很多面向對象編程語言中“淺拷貝”和“深拷貝”的區別類似。對於基本數據類型來說,“深拷貝”和“淺拷貝“產生的效果相同。對於引用對象類型來說,”淺拷貝“更像僅僅拷貝了對象的內存地址。
如果我們想實現對String的”深拷貝“怎麼辦? 可以直接調用String的Clone特性實現對內存的值拷貝而不是簡單的地址拷貝。
#![allow(unused)] fn main() { { let a: String = String::from("xyz"); let b = a.clone(); // <-注意此處的clone println!("{}", a); } }
這個時候可以編譯通過,並且成功打印"xyz"。
clone後的效果等同如下:
Before move:
a <=> 內存(地址:A,內容:"xyz")
After move:
a <=> 內存(地址:A,內容:"xyz")
b <=> 內存(地址:B,內容:"xyz")
注意,然後a和b對應的資源值相同,但是內存地址並不一樣。
可變性
通過上面,我們已經已經瞭解了變量聲明、值綁定、以及移動move語義等等相關知識,但是還沒有進行過修改變量值這麼簡單的操作,在其他語言中看似簡單到不值得一提的事卻在Rust中暗藏玄機。 按照其他編程語言思維,修改一個變量的值:
#![allow(unused)] fn main() { let a: i32 = 100; a = 200; }
很抱歉,這麼簡單的操作依然還會報錯:
error: re-assignment of immutable variable
a[E0384]:3 a = 200;
不能對不可變綁定賦值。如果要修改值,必須用關鍵字mut聲明綁定為可變的:
#![allow(unused)] fn main() { let mut a: i32 = 100; // 通過關鍵字mut聲明a是可變的 a = 200; }
想到“不可變”我們第一時間想到了const常量,但不可變綁定與const常量是完全不同的兩種概念;首先,“不可變”準確地應該稱為“不可變綁定”,是用來約束綁定行為的,“不可變綁定”後不能通過原“所有者”更改資源內容。
例如:
#![allow(unused)] fn main() { let a = vec![1, 2, 3]; //不可變綁定, a <=> 內存區域A(1,2,3) let mut a = a; //可變綁定, a <=> 內存區域A(1,2,3), 注意此a已非上句a,只是名字一樣而已 a.push(4); println!("{:?}", a); //打印:[1, 2, 3, 4] }
“可變綁定”後,目標內存還是同一塊,只不過,可以通過新綁定的a去修改這片內存了。
#![allow(unused)] fn main() { let mut a: &str = "abc"; //可變綁定, a <=> 內存區域A("abc") a = "xyz"; //綁定到另一內存區域, a <=> 內存區域B("xyz") println!("{:?}", a); //打印:"xyz" }
上面這種情況不要混淆了,a = "xyz"表示a綁定目標資源發生了變化。
其實,Rust中也有const常量,常量不存在“綁定”之說,和其他語言的常量含義相同:
#![allow(unused)] fn main() { const PI:f32 = 3.14; }
可變性的目的就是嚴格區分綁定的可變性,以便編譯器可以更好的優化,也提高了內存安全性。
高級Copy特性
在前面的小節有簡單瞭解Copy特性,接下來我們來深入瞭解下這個特性。 Copy特性定義在標準庫std::marker::Copy中:
#![allow(unused)] fn main() { pub trait Copy: Clone { } }
一旦一種類型實現了Copy特性,這就意味著這種類型可以通過的簡單的位(bits)拷貝實現拷貝。從前面知識我們知道“綁定”存在move語義(所有權轉移),但是,一旦這種類型實現了Copy特性,會先拷貝內容到新內存區域,然後把新內存區域和這個標識符做綁定。
哪些情況下我們自定義的類型(如某個Struct等)可以實現Copy特性? 只要這種類型的屬性類型都實現了Copy特性,那麼這個類型就可以實現Copy特性。 例如:
#![allow(unused)] fn main() { struct Foo { //可實現Copy特性 a: i32, b: bool, } struct Bar { //不可實現Copy特性 l: Vec<i32>, } }
因為Foo的屬性a和b的類型i32和bool均實現了Copy特性,所以Foo也是可以實現Copy特性的。但對於Bar來說,它的屬性l是Vec<T>類型,這種類型並沒有實現Copy特性,所以Bar也是無法實現Copy特性的。
那麼我們如何來實現Copy特性呢?
有兩種方式可以實現。
-
通過
derive讓Rust編譯器自動實現#![allow(unused)] fn main() { #[derive(Copy, Clone)] struct Foo { a: i32, b: bool, } }編譯器會自動檢查
Foo的所有屬性是否實現了Copy特性,一旦檢查通過,便會為Foo自動實現Copy特性。 -
手動實現
Clone和Copytrait#[derive(Debug)] struct Foo { a: i32, b: bool, } impl Copy for Foo {} impl Clone for Foo { fn clone(&self) -> Foo { Foo{a: self.a, b: self.b} } } fn main() { let x = Foo{ a: 100, b: true}; let mut y = x; y.b = false; println!("{:?}", x); //打印:Foo { a: 100, b: true } println!("{:?}", y); //打印:Foo { a: 100, b: false } }從結果我們發現
let mut y = x後,x並沒有因為所有權move而出現不可訪問錯誤。 因為Foo繼承了Copy特性和Clone特性,所以例子中我們實現了這兩個特性。
高級move
我們從前面的小節瞭解到,let綁定會發生所有權轉移的情況,但ownership轉移卻因為資源類型是否實現Copy特性而行為不同:
#![allow(unused)] fn main() { let x: T = something; let y = x; }
- 類型
T沒有實現Copy特性:x所有權轉移到y。 - 類型
T實現了Copy特性:拷貝x所綁定的資源為新資源,並把新資源的所有權綁定給y,x依然擁有原資源的所有權。
move關鍵字
move關鍵字常用在閉包中,強制閉包獲取所有權。
例子1:
fn main() { let x: i32 = 100; let some_closure = move |i: i32| i + x; let y = some_closure(2); println!("x={}, y={}", x, y); }
結果: x=100, y=102
注意: 例子1是比較特別的,使不使用 move 對結果都沒什麼影響,因為x綁定的資源是i32類型,屬於 primitive type,實現了 Copy trait,所以在閉包使用 move 的時候,是先 copy 了x ,在 move 的時候是 move 了這份 clone 的 x,所以後面的 println!引用 x 的時候沒有報錯。
例子2:
fn main() { let mut x: String = String::from("abc"); let mut some_closure = move |c: char| x.push(c); let y = some_closure('d'); println!("x={:?}", x); }
報錯: error: use of moved value:
x[E0382]:5 println!("x={:?}", x);
這是因為move關鍵字,會把閉包中的外部變量的所有權move到包體內,發生了所有權轉移的問題,所以println訪問x會如上錯誤。如果我們去掉println就可以編譯通過。
那麼,如果我們想在包體外依然訪問x,即x不失去所有權,怎麼辦?
fn main() { let mut x: String = String::from("abc"); { let mut some_closure = |c: char| x.push(c); some_closure('d'); } println!("x={:?}", x); //成功打印:x="abcd" }
我們只是去掉了move,去掉move後,包體內就會對x進行了可變借用,而不是“剝奪”x的所有權,細心的同學還注意到我們在前後還加了{}大括號作用域,是為了作用域結束後讓可變借用失效,這樣println才可以成功訪問並打印我們期待的內容。
關於“Borrowing借用”知識我們會在下一個大節中詳細講解。