Rust 所有權(二):Move Data
下面是一段正確的 Rust 代碼,定義變量 x 並賦值一個字符串,再將 x 賦值給變量 y,最後打印 y 會輸出字符串 “Hello, World!”:
fn main() {
let x = String::from("Hello, World!");
let y = x;
println!("{}", y); // 打印 y
}
這段代碼的行爲和其它大多數編程語言無異。
那麼,再來看看下面這段錯誤的代碼,它無法通過編譯:
// 編譯錯誤
fn main() {
let x = String::from("Hello, World!");
let y = x;
println!("{}", x); // 打印 x
// ^ value borrowed here after move
}
這段代碼的行爲可就與大多數編程語言不同了。比如,在 JavaScript 語言中是允許這樣寫的:
// JavaScript
function main() {
let x = 'Hello, World!';
let y = x;
console.log(x);
}
問題出在哪?
其實,編譯器的錯誤消息已經給出了答案,是因爲 “借用了已經移動(move)了的值 x”。暫且不管什麼是借用,只關注 move(數據移動)。數據移動是 Rust 中數據交互方式之一。在上例中,它指的是字符串 "Hello, World!" 的所有權從變量 x 移動到了變量 y。在移動之後,變量 x 變爲未初始化狀態,因此不允許再使用變量 x,除非給 x 再重新賦值。
爲什麼要有 move 操作?
因爲要遵守所有權規則。在同一時刻,一個值只能有一個所有者,即指向它的變量。上例中,第 3 行 "Hello, World!" 的所有者是變量 x,第 4 行 "Hello, World!" 的所有者是 y。反觀上例的 JavaScript 代碼,最後相當於 "Hello, World!" 的所有者有 2 個,即 x 和 y。
只有唯一確定的所有者的好處是:內存安全。編譯器在編譯階段就可以確定在所有者出作用域時釋放內存資源,並自動生成管理內存的代碼。如果像 JavaScript 語言一樣是共享所有權的,那麼或者需要程序員對程序完全掌控,清楚地知道有多少個所有者、何時可以安全地釋放以及不能重複釋放等問題;或者採納 Linus 的建議,“選擇一門支持自動內存管理的編程語言”,例如 JavaScript。
只有唯一確定的所有者的壞處是:操作麻煩。例如,有這樣一道算法題,要求刪除單鏈表中第 N 個結點。學過一些數據結構的同學能夠明白,這道題的關鍵點是修改被刪除結點前後的 next 指針,以及要考慮刪除首、尾結點這兩個特殊情況。如果用 Rust 來實現,那麼還額外需要在操作鏈表時保證所有權規則!在修改結點時,不允許出現一個結點有兩個所有者的情況!例如,下圖中紅框處出現的情形可能意味着 Node3 有兩個所有者,這是不允許的。
數據 copy
不是任何類型的數據都是非 move 不可的。如果有些數據不用 move 就能滿足所有權規則不會對程序產生明顯的影響並且簡單易操作,那麼何樂而不爲呢。看如下的代碼:
fn main() {
let x = 666;
let y = x;
println!("{}", x);
println!("{}", y);
}
此例中的代碼是正確的,這也意味着這裏沒發生 move,否則打印 x 會報錯。這裏發生了數據 copy。數值 666 被拷貝了一份賦值給了 y。此時,x 和 y 分別保存了自己的數值 666,每個 666 都只有一個所有者。
爲什麼 “Hello, World” 是 move,而 666 是 copy?
因爲數值 666 在編譯時就已知其佔用的空間,並且不會改變。它被完全保存在棧內存上,可以快速地、安全地按位複製。所以它是 copy 的。字符串是稍微複雜的數據類型,字符串的長度在程序運行中是可以動態改變的。因此,字符串默認使用堆和棧內存來保存。它不能安全地按位拷貝,因爲會拷貝出多個指向堆中字符串的指針,違反了所有權規則。如果非要拷貝,必須進行 “深拷貝”,在 Rust 裏叫做 clone。
我怎麼知道誰是 copy 的,誰是 move 的?
最靠譜的方法是查看官方文檔,比如想知道 i32 是 copy 還是 move,就去查 i32 的文檔。在 Rust 裏擁有確定大小的標量類型是 copy 的,例如布爾類型、浮點型、整型等。
本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源:https://mp.weixin.qq.com/s/InjA92NjMB-Oz2Orh9lLCw