所有權(Ownership)
在進入正題之前,大家先回憶下一般的編程語言知識。 對於一般的編程語言,通常會先聲明一個變量,然後初始化它。 例如在C語言中:
int* foo() {
int a; // 變量a的作用域開始
a = 100;
char *c = "xyz"; // 變量c的作用域開始
return &a;
} // 變量a和c的作用域結束
儘管可以編譯通過,但這是一段非常糟糕的代碼,現實中我相信大家都不會這麼去寫。變量a和c都是局部變量,函數結束後將局部變量a的地址返回,但局部變量a
存在棧中,在離開作用域後,局部變量所申請的棧上內存都會被系統回收,從而造成了Dangling Pointer
的問題。這是一個非常典型的內存安全問題。很多編程語言都存在類似這樣的內存安全問題。再來看變量c
,c
的值是常量字符串,存儲於常量區,可能這個函數我們只調用了一次,我們可能不再想使用這個字符串,但xyz
只有當整個程序結束後系統才能回收這片內存,這點讓程序員是不是也很無奈?
備註:對於
xyz
,可根據實際情況,通過堆的方式,手動管理(申請和釋放)內存。
所以,內存安全和內存管理通常是程序員眼中的兩大頭疼問題。令人興奮的是,Rust卻不再讓你擔心內存安全問題,也不用再操心內存管理的麻煩,那Rust是如何做到這一點的?請往下看。
綁定(Binding)
重要:首先必須強調下,準確地說Rust中並沒有變量這一概念,而應該稱為標識符
,目標資源
(內存,存放value)綁定
到這個標識符
:
{
let x: i32; // 標識符x, 沒有綁定任何資源
let y: i32 = 100; // 標識符y,綁定資源100
}
好了,我們繼續看下以下一段Rust代碼:
{
let a: i32;
println!("{}", a);
}
上面定義了一個i32類型的標識符a
,如果你直接println!
,你會收到一個error報錯:
error: use of possibly uninitialized variable:
a
這是因為Rust並不會像其他語言一樣可以為變量默認初始化值,Rust明確規定變量的初始值必須由程序員自己決定。
正確的做法:
{
let a: i32;
a = 100; //必須初始化a
println!("{}", a);
}
其實,let
關鍵字並不只是聲明變量的意思,它還有一層特殊且重要的概念-綁定。通俗的講,let
關鍵字可以把一個標識符和一段內存區域做“綁定”,綁定後,這段內存就被這個標識符所擁有,這個標識符也成為這段內存的唯一所有者。
所以,a = 100
發生了這麼幾個動作,首先在棧內存上分配一個i32
的資源,並填充值100
,隨後,把這個資源與a
做綁定,讓a
成為資源的所有者(Owner)。
作用域
像C語言一樣,Rust通過{}
大括號定義作用域:
{
{
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)
先看如下代碼:
{
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
關鍵字完成,和綁定不同的是,=
兩邊的左值和右值均為兩個標識符:
語法:
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後是可以訪問的”:
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特性實現對內存的值拷貝而不是簡單的地址拷貝。
{
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中暗藏玄機。 按照其他編程語言思維,修改一個變量的值:
let a: i32 = 100;
a = 200;
很抱歉,這麼簡單的操作依然還會報錯:
error: re-assignment of immutable variable
a
[E0384]
:3 a = 200;
不能對不可變綁定賦值。如果要修改值,必須用關鍵字mut聲明綁定為可變的:
let mut a: i32 = 100; // 通過關鍵字mut聲明a是可變的
a = 200;
想到“不可變”我們第一時間想到了const
常量,但不可變綁定與const
常量是完全不同的兩種概念;首先,“不可變”準確地應該稱為“不可變綁定”,是用來約束綁定行為的,“不可變綁定”後不能通過原“所有者”更改資源內容。
例如:
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去修改這片內存了。
let mut a: &str = "abc"; //可變綁定, a <=> 內存區域A("abc")
a = "xyz"; //綁定到另一內存區域, a <=> 內存區域B("xyz")
println!("{:?}", a); //打印:"xyz"
上面這種情況不要混淆了,a = "xyz"
表示a
綁定目標資源發生了變化。
其實,Rust中也有const常量,常量不存在“綁定”之說,和其他語言的常量含義相同:
const PI:f32 = 3.14;
可變性的目的就是嚴格區分綁定的可變性,以便編譯器可以更好的優化,也提高了內存安全性。
高級Copy特性
在前面的小節有簡單瞭解Copy特性,接下來我們來深入瞭解下這個特性。 Copy特性定義在標準庫std::marker::Copy中:
pub trait Copy: Clone { }
一旦一種類型實現了Copy特性,這就意味著這種類型可以通過的簡單的位(bits)拷貝實現拷貝。從前面知識我們知道“綁定”存在move語義(所有權轉移),但是,一旦這種類型實現了Copy特性,會先拷貝內容到新內存區域,然後把新內存區域和這個標識符做綁定。
哪些情況下我們自定義的類型(如某個Struct等)可以實現Copy特性? 只要這種類型的屬性類型都實現了Copy特性,那麼這個類型就可以實現Copy特性。 例如:
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編譯器自動實現#[derive(Copy, Clone)] struct Foo { a: i32, b: bool, }
編譯器會自動檢查
Foo
的所有屬性是否實現了Copy
特性,一旦檢查通過,便會為Foo
自動實現Copy
特性。手動實現
Clone
和Copy
trait#[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
特性而行為不同:
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借用”知識我們會在下一個大節中詳細講解。