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