Rust:內存、所有權和借用
棧
在計算機科學中,堆棧是一種抽象數據類型,用作元素的集合,具有兩個主要操作:Push(將一個元素添加到集合) 和 Pop(刪除最近添加的尚未刪除的元素)——維基百科。
堆棧的意思是把對象放在另一個對象的上面。當我們添加一個對象時,我們將它添加到頂部。當我們刪除一個對象時,我們也從上面刪除。
棧幀
我們的代碼,在執行方面,它被加載在堆棧中。所以,對於 Rust,我們從主函數開始。我們把 main 函數放到堆棧上。
怎麼說呢?假設我們的棧幀是一張紙,函數體在這張紙上。想象我們的處理器是一個人,他讀着面前的這張紙,然後按照指令去做。
換句話說,當前的棧幀也是作用域。
函數被調用時
當一個函數被調用時,也就是把我們的紙放到堆棧頂部的時候。讓我們看看下面的代碼:
fn main() {
println!("Hello, World");
let x = 10;
println("{}", x);
print_another_variable();
}
fn print_another_variable() {
let k = 11;
println!("{}",k)
}
我們的第一個棧幀是 main() 函數體,它寫入 “Hello, World”,然後將值 10 賦給 x,打印它。然後調用另一個函數 print_another_variable()。
print_another_variable() 寫在另一張紙上,當我們在 main 函數中調用它時,我們將 print_another_variable() 紙放在 main() 紙的上面。
功能完成後,將這張紙撕開。回到調用這個函數的行,繼續向前。
現在,當我們往棧上放東西時,我們需要告訴它我們需要多少內存。以 i32 爲例,其大小是已知的,它是一個 32 位有符號整數。
一個整數的最大值是 2147483647,如果再加 1,就會溢出來,放更大的值也不能變成 33 位,因爲它總是 32 位的。
未知大小
當大小已知時,將東西放入堆棧可以正常工作。但是當我們不知道大小的時候呢?比如一個切片或者向量,或者字符串 (不是字符)。
讓我們考慮一下,我們想要存儲動態值,其值可以增長或縮小。以一個數組爲例,數組的長度是固定的,如 array[5] 最多可以容納 5 個元素。
但是一個向量可以增長,你可以從 5 個元素開始,最終有 N 個元素,其中 N 小於等於或大於 5。但是我們不能要求堆棧給我們無限的空間。
對於這些問題,我們不直接把可增長的數據放到棧中,我們把可增長的動態數據放到其他地方。然後,我們生成一個指針,它的大小是已知的,我們把這個指針放到棧中。
堆
我們的堆沒有棧那麼快速和有效,但它支持處理動態內容,如字符串或向量。
讓我們看看下面的代碼:
fn main() {
let a = String::from("Hello, World");
}
對於這段代碼,我們告訴堆將 “Hello, World” 字符串存儲到內存中,它根據我們當前擁有的空間來檢查它需要多少空間,有 12 個字符(包括逗號和空格)。
堆接受內存地址 0xAB0001(假設),並開始放置我們的字符串。想象這個序列是這樣的 0xAB0001, 0xAB0002…0xABN。
hello world 字符串結束於 0xAB0012。但是我們在堆棧中存儲什麼呢?我們的堆棧包含以下值:
ptr = 0xAB0001 # a pointer to our first memory address
cap = 12 # cap stands for capacity
len = 12 # len stands for length
如果我們將字符串更新爲 Hello,我們的容量仍然是 12,但長度將是 5。
我們知道指針變量本身的大小。如果我們分解容量和長度,我們也知道它們的值大小。
現在,讓我們來看看 rust 告訴我們的 3 條所有權規則。
所有權規則
-
Rust 中的每個值都有一個名爲所有者的變量。
-
一次只能有一個所有者。
-
當所有者超出範圍時,該值將被刪除。
Copy & Pointer
讓我們看一下這個例子:
let x = 100;
let y = x;
這將觸發一個 COPY,複製值 100。因爲 x 在這裏是一個整數,換句話說,是一個已知大小的變量。其他已知大小的變量也一樣,到處複製它們既廉價又快捷。
但是,對於下面的代碼:
let a = String::from("Hello");
let b = a;
因此,像前面一樣,a 應該是指向堆內存的指針,保存數據 “Hello”。如果是這樣,現在當我們賦值 b 時,如果它像我們對整數或已知大小的數據類型所做的那樣複製指針,那麼我們的“Hello” 將有兩個所有者。
所有者和雙重釋放
對於上面提到的類比,Hello 有兩個所有者。但如果一個改變了,另一個就無法知道事情發生了變化。
也許我們可以把它改成 “Hello, Universe, this is from the Milky Way galaxy.”,容量和長度都發生了變化。但對於 b,它還不知道!指針不同步。
現在是時候清理內存了,所有者 a 和 b 都試圖清理內存。這將觸發錯誤,因爲假設 a 首先清除內存,然後 b 嘗試清除內存,但它已經被清理了!。
我們考慮另一種情況,我們的容量是 12。現在我們分配一個非常長的字符串,並使字符串的長度 (和容量) 爲 500。
現在,CPU 看到 0xAB0001 沒有 500 個連續空閒內存塊,由於空間不可用,它將值重新分配內存地址。但是對於 b,沒有辦法知道指針不應該再指向 0xAB0001。
因此對於 rust,當賦值 “let b = a” 時,其中 a 是字符串或未知大小的數據類型,它會觸發移動而不是複製。也就是說,存儲在 a 中的指針信息,現在存儲在 b 中,而 a 沒有這個信息。
如果我們運行下面的程序,它會導致一個錯誤,說 “發生了移動,是因爲 a 是類型 String,它沒有實現 Copy。”
let a = String::from("Hello");
println!("{}",a);
let b = a;
println!("b = {}",b);
println!("a = {}",a);
如果我們想複製實際值 “Hello”,我們需要做一個克隆,如 “let b = a.clone()”。
超出作用域範圍
看看下面這些代碼:
fn main() {
let a = String::from("Hello");
print_something(a);
println!("a in main => {}", a);
}
fn print_something(a : String) {
println!("inside the function => {}", a);
}
這裏發生了什麼?這個程序將拋出一個錯誤。一旦 a 的所有權被傳遞給 print_something,並且 print_something 已經被執行,它就超出了作用域。由於函數不返回任何東西,它內部的所有值都被刪除了。
因此,在 main 函數中,a 沒有任何價值,我們放棄了所有權。現在運行 println!("a in main => {}", a) 後將觸發一個錯誤。
但是如果我們在 println 之後調用 print_something() 函數,那麼在 println 期間就不會有問題,因爲 a 變量仍然保存着信息。
歸還所有權
另一種解決方法是從函數中返回一個值。使用下面的代碼,它將正常運行。
fn main() {
// added `mut` here
let mut a = String::from("Hello");
a = print_something(a);
println!("a in main => {}", a);
}
// Notice the " -> String", it means it is gonna return a string
fn print_something(a : String) -> String {
println!("inside the function => {}", a);
a // here we are returning a string value
}
我們需要添加 mut,因爲 rust 的變量在默認情況下是不可變的。但這裏的關鍵點是在 a = print_something(a) 中,我們從 print_something() 函數中獲得一個字符串。
借用 | 傳遞引用
另一個解決方案是傳遞引用。請看下面的例子:
fn main() {
let a = String::from("Hello");
// we are passing a reference, denoted by '&'
print_something(&a);
println!("a in main => {}", a);
}
// see I've added '&' before String
fn print_something(a : &String) {
println!("inside the function => {}", a);
}
在同一時間只能有一個可變借用
你可以有多個不可變的或只讀的借用,但你不能在同一時間擁有多個可變借用。
let mut a = String::from("a");
let b = &mut a;
let c = &mut a;
它會拋出一個錯誤。回到同樣的問題,假設 b 改變了一些數據,但是 c 不知道。
借用 VS 移動
借用將數據保持在棧上的同一位置,只傳遞內存指針,但 “移動” 總是將數據從棧的一個位置複製到另一個位置。因此,只能移動大小已知的數據,這可能會導致性能問題。通過這種棧的工作方式自動保證了生命週期。
多重可變
從技術上講,Rust 只提供了一個可變借用,但是使用 Cell,你可以有多個 & Cell,它允許使用 Cell::::set(&self, T) 相關函數對內容進行突變。
本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源:https://mp.weixin.qq.com/s/aFtot5XUKCH5KLgPTDft_g