理解 Rust 中的所有權

本文譯者爲 360 奇舞團前端開發工程師

原文標題:Understanding ownership in Rust

原文作者:Ukpai Ugochi

原文鏈接:https://blog.logrocket.com/understanding-ownership-in-rust/

在 Stack Overflow 進行的開發人員調查中,Rust 連續第五年成爲最受歡迎的編程語言。開發者喜愛 Rust 的原因有很多,其中之一就是它的內存安全保證。

Rust 通過稱爲所有權的特性來保證內存安全。所有權與其他語言中的垃圾收集器的工作方式不同,因爲它只包含編譯器需要在編譯時檢查的一組規則。如果不遵守所有權規則,編譯器將不會編譯。borrow checker 確保你的代碼遵循所有權規則。

對於沒有垃圾收集器的語言,你需要顯式分配和釋放內存空間。當涉及大型代碼庫時,這很快就會變得乏味和具有挑戰性。

值得慶幸的是, 內存管理由 Rust 編譯器使用所有權模型處理。Rust 編譯器會自動插入一個 drop 語句來釋放內存。它使用所有權模型來決定在哪裏釋放內存;當所有者超出範圍時,內存將被釋放。

fn main() {
    {
    let x = 5 ;
    // x 被丟棄在這裏,因爲它超出了範圍
    }
}

什麼是棧和堆?

棧和堆都是可供你的代碼在運行時使用的內存存儲段。對於大多數編程語言,開發人員通常不關心棧和堆上的內存分配情況。但是,由於 Rust 是一種系統編程語言,因此值的存儲方式(在棧或堆中)對於語言的行爲方式至關重要。

內存是如何存儲在棧中的呢?假設桌子上有一疊書, 這些書的排列方式是最後一本書放在書堆的頂部,第一本書在底部。理想情況下,我們不想從書堆下面滑出最下面的書,從上面挑一本書來閱讀會更容易。

這正是內存在棧中的存儲方式;它使用後進先出的方法。在這裏,它按照獲取值的順序存儲值,但以相反的順序刪除它們。同樣重要的是要注意存儲在棧中的所有數據在編譯時都具有已知大小。

堆中的內存分配與棧中的內存分配方式不同。假設你要爲朋友買一件襯衫, 但是你不知道朋友穿的襯衫具體尺碼, 但經常看到他,你認爲他可能是 M 碼或 L 碼。雖然你不完全確定,但你購買大號是因爲即使他是中號,你的朋友仍然能穿上它。這是棧和堆之間的一個重要區別:我們不需要知道存儲在堆中的值的確切大小。

與棧相比,堆中沒有組織。將數據推入和推出棧很容易,因爲一切都是有組織的並遵循特定的順序。系統理解,當你將一個值壓入棧時,它會停留在頂部,而當你需要從棧中取出一個值時,你正在檢索存儲的最後一個值。

然而,堆中的情況並非如此。在堆上分配內存需要動態地搜索一塊足夠大的內存空間來滿足分配要求,並且返回指向該內存位置的地址。檢索值時,需要使用指針來找到存儲該值的內存位置。

在堆上分配看起來像書籍索引,其中存儲在堆中的值的指針存儲在棧中。但是,分配器還需要搜索一個足夠大的空白空間來包含該值。

函數的局部變量存儲在函數棧中,而數據類型(如 String、Vector、Box 等)存儲在堆中。瞭解 Rust 的內存管理以確保應用程序按預期運行非常重要。

所有權規則

所有權有三個基本規則來預測內存如何存儲在棧和堆中:

  1. 每個 Rust 值都有一個稱爲其 “所有者” 的變量:
let x = 5 ; // x is the owner of the value "5"
  1. 每個值一次只能有一個所有者

  2. 當所有者超出範圍時,該值將被刪除:

fn main () { 
    { // // scope begins
        let s = String :: from ( "hello" ); // s comes into scope
    }  
    // the value of s is dropped at this point, it is out of scope
}

所有權如何運作

在我們的介紹中,我們建立了一個事實,即所有權不像垃圾收集器系統。大多數編程語言要麼使用垃圾收集器,要麼要求開發人員自己分配和釋放內存。

在所有權中,我們爲自己請求內存,當所有者超出範圍時,該值將被刪除並釋放內存。這正是所有權規則第三條所解釋的。爲了更好地理解這是如何工作的,讓我們看一個例子:

fn main () {
    { 
        // a is not valid here
        let a = 5 ; // a is valid here
        // do stuff with a
    } 
    println!("{}", a) // a is no longer valid at this point, it is out of scope
}

這個例子非常簡單;這就是棧中內存分配的工作原理。a 由於我們知道它的值將佔用的確切空間,因此在棧上分配了一塊內存 ( ) 5。然而,情況並非總是如此。有時,你需要爲一個在編譯時不知道其大小的可增長值分配內存空間。

對於這種情況,內存是在堆上分配的,你首先必須請求內存,如下例所示:

fn main () { 
    { 
        letmut s = String :: from ( "hello" ); // s is valid from this point forward
        push_str ( ", world!" ); // push_str() appends a literal to a String
    	println !( "{}" , s ); // This will print `hello, world!`
    } 
    // s is no longer valid here
}

我們可以根據需要附加任意多的字符串,s 因爲它是可變的,因此很難知道編譯時所需的確切大小。因此在我們的程序中我們需要一個字符串大小的內存空間:

克隆和複製

在本節中,我們將研究所有權如何影響 Rust 中的某些功能,從 clone 和 copy 功能開始。

對於具有已知大小的值(如 )integers,將值複製到另一個值會更容易。例如:_

fn main() {
    let a = "5" ; 
    let b = a ; // copy the value a into b
    println !( "{}" , a ) // 5 
    println !( "{}" , b ) // 5 
}

因爲 a 存儲在棧中,所以更容易複製它的值來爲 製作另一個副本 b。對於存儲在堆中的值,情況並非如此:

fn main () { 
    let a = String :: from ( "hello" ); 
    let b = a ; // copy the value a into b
    println !( "{}" , a ) // This will throw an error because a has been moved or ownership has been transferred
    println !( "{}" , b ) // hello 
}

當你運行該命令時,你將收到一個錯誤 error[E0382]: borrow of moved value: "a"move

所有權和函數

將值傳遞給函數遵循相同的所有權規則,這意味着它們一次只能有一個所有者,並且一旦超出範圍就釋放內存。讓我們看看這個例子:

fn main() {
    let s1 = givesOwnership(); // givesOwnership moves its return

    let s2 = String::from("hello"); // s2 comes into scope
    let s3 = takesAndGivesBack(s2); // s2 is moved into takesAndGivesBack, 
                                    // which also moves its return value into s3

} // Here, s3 goes out of scope and is dropped. s2 goes out of scope but was
  // moved, so nothing happens. s1 goes out of scope and is dropped.


fn givesOwnership() -> String { // givesOwnership will move its
                                // return value into the function
                                // that calls it

    let someString = String::from("hello");  // someString comes into scope

    someString                               // someString is returned and
                                             // moves out to the calling
                                             // function
}

// takesAndGivesBack will take a String and return one
fn takesAndGivesBack(aString: String) -> String { // aString comes into
                                                      // scope

    aString  // aString is returned and moves out to the calling function
}

切片

引用序列中彼此相鄰的元素,而不是引用整個集合。因此,可以使用切片類型。但是,此功能不具有引用和借用之類的所有權。

讓我們看看下面的例子。在此示例中,我們將使用切片類型來引用連續序列中值的元素:

fn main() {
    let s = String::from("Nigerian");
    // &str type
    let a = &s[0..4];// doesn't transfer ownership, but references/borrow the first four letters.
    let b = &s[4..8]; // doesn't transfer ownership, but references/borrow the last four letters.
    println!("{}", a); // prints Nige

    println!("{}", b); // prints rian
    
    let v=vec![1,2,3,4,5,6,7,8];

    // &[T] type
    let a = &v[0..4]; // doesn't transfer ownership, but references/borrow the first four element.
    let b = &v[4..8]; // doesn't transfer ownership, but references/borrow the last four element.
    println!("{:?}", a); // prints [1, 2, 3, 4]
    println!("{:?}", b); // prints [5, 6, 7, 8]
    
}

結論

所有權是 Rust 的一個重要特性。掌握所有權的概念,有利於編寫可擴展的代碼。很多人喜歡 Rust 的原因就是因爲這個特性,一旦你掌握了它,你就可以更高效的編寫代碼。

在本文中,我們介紹了所有權的基礎知識及其規則,以及如何應用它們。除此之外,還介紹了 Rust 的一些不涉及到所有權的特性以及如何巧妙地使用它們。對於 Rust 的所有權特性感興趣的讀者朋友,可以查看相關文檔。

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