Rust 如何保證內存安全 - 2

簡介

在上一篇文章中,我討論了內存安全性的概念以及不同語言實現內存管理的不同技術。幾乎所有的語言都只能屬於一個範圍,要麼是語言運行時進行內存管理,保證內存安全;要麼是程序員控制內存,不能保證內存安全。Rust 的獨特之處在於它沒有進行這種權衡——程序員可以同時獲得內存安全和內存控制。

別名、可變和安全性

要安全地釋放一個對象,就必須沒有對它的引用,否則就會出現一個懸垂指針。類似地,如果一個線程想把一個對象發送給另一個線程,在發送線程上就不能有對它的引用。這裏有兩個元素:別名和可變。 如果對象沒有被銷燬或通過線程發送,那麼引用它並沒有什麼問題,只有當兩者結合在一起時,你纔會遇到麻煩。

根據這個觀察,Rust 的內存安全解決方案是簡單的,同時不允許別名和可變,Rust 通過所有權和借用實現了這一點。

所有權

當你在 Rust 中創建一個新對象時,被賦值的變量成爲對象的所有者。例如在下面的 Rust 代碼中,變量 v 擁有 Vec 實例:

let v: Vec<i32> = Vec::new();

當 v 超出範圍時,Vec 就會被丟棄。一次只能有一個對象的所有者,這可以確保只有所有者才能釋放它。這避免了重複釋放內存的錯誤。如果 v 被賦值給另一個變量,所有權轉移:

let v1 = v;//v1 is the new owner

因爲 v1 現在是所有者,所以不再允許通過 v 訪問:

v.len();//error: Use of moved value

所有者當然可以改變對象:

let mut v = Vec::new();//mut is needed to mutate the object
v.push(1);

如果一個程序員在 Rust 中所能做的只是創建值的所有者並將它們轉移,那麼它將是一個非常受限制的編程環境。幸運的是,Rust 允許向所有者借用。

借用

借用就像別名,可以向所有者借用值:

let v: Vec<i32> = Vec::new();
let v1 = &v;//v1 has borrowed from v
v.len();//fine
v1.len();//also fine

與所有者不同,可以同時有多個不可變借用:

let v: Vec<i32> = Vec::new();
let v1 = &v;//v1 has borrowed from v
let v2 = &v;//v2 has also borrowed from v
v.len();//allowed
v1.len();//also allowed
v2.len();//also allowed

但是,在所有者銷燬資源後,借用無法訪問該資源,否則將導致 “內存釋放後再使用” 的 bug:

let v1: &Vec<i32>;
{
   let v = Vec::new();
   v1 = &v;
}//v is dropped here
v1.len();//error:borrowed value does not live long enough

直到現在,所有的借用都是不可變的。可以有可變的引用,但是正如我接下來要展示的,當引入可變時,Rust 足夠聰明,它不允許不可變借用與可變借用混淆使用。

可變借用

雖然可以有多個共享引用,但在同一時間只能有一個可變引用:

let mut v:Vec<i32> = Vec::new();
let v1 = &mut v;//first mutable reference
let v2 = &mut v;//second mutable reference
v1.push(1);//error:cannot borrow `v` as mutable more than once at a time

只要通過可變引用允許改變,Rust 就通過禁止其他引用 (共享的或可變的) 來消除別名。

這些借用規則防止懸垂指針。如果 Rust 同時允許一個可變的引用和一個不可變的引用,那麼當不可變的引用指向一塊內存時,這塊內存有可能會通過可變引用變成無效的。例如,在下面的代碼中,如果允許這樣的代碼,v1 可以訪問無效內存:

let mut v = vec![0, 1, 2, 3];
let v1 = &v[0];//an immutable reference to Vec's first element
v.push(4);//this can invalidate Vec's internal buffer
let v2 = *v1;//this could access invalid memory

相比較下,在 C++ 中這種相似的代碼是被允許的。

生命週期

Rust 通過跟蹤變量的生命週期來做到以上幾點,變量的生命週期與其作用域綁定:

let v1: &Vec<i32>;//-------------------------+
{//                                          |
   let v = Vec::new();//-----+               |v1's lifetime
   v1 = &v;//                | v's lifetime  |
}//<-------------------------+               |
v1.len();//<---------------------------------+

因此,編譯器比較各種變量的生存期,以找出是否有可疑的事情發生。例如,在上面的代碼中,v1 比所有者 v 活得更久,這是不允許的。上面例子中的生存期稱爲詞法生存期,因爲它們是從變量作用域推斷出來的。實際上,Rust 有一個更復雜的生命週期實現,稱爲非詞彙生命週期。

總結

在這篇文章中,我討論了所有權和借用的概念,以及它們如何幫助 Rust 實現內存安全。許多內存安全問題可以歸結爲這樣一個事實:像 c++ 這樣的語言允許別名和可變同時存在。而 Rust 在編譯時檢測這些內存安全問題,這種能力使它成爲系統編程語言的有力競爭者。

本文翻譯自:

https://hashrust.com/blog/memory-safety-in-rust-part-2/

coding 到燈火闌珊 專注於技術分享,包括 Rust、Golang、分佈式架構、雲原生等。

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/ovbErqLte-iyAEsGdifmWA