通俗易懂解釋Rust所有權和借用概念

banq發表於2022-05-30

Rust有三個主要概念:
  • 所有權(在同一時間只有一個變數 "擁有 "資料,並且所有者負責取消分配)
  • 借用(你可以向擁有的變數借用一個引用)
  • 生命週期(所有的資料都會跟蹤它將被銷燬的時間)

這些都是相當簡單的概念,但它們往往與其他語言的概念背道而馳,所以我想試著更詳細地解釋一下這些概念。因為Rust並不是唯一使用這些概念的語言(例如,你可以在C++中做unique_ptr),學習這些概念不僅可以幫助你寫出更好的Rust程式碼,而且可以寫出更好的程式碼。

所有權
所有權的概念是,如果你擁有一個專案,你就負責在你完成後銷燬這個專案。

在Rust中,一塊資料在任何時候都只能有一個所有者。這與垃圾收集語言或帶有原始指標的語言有很大的不同,因為你經常要 "玩弄 "資料,而不是對同一資料有多個引用,所以在同一時間只有一個變數擁有該資料。

擁有的資料只有在擁有的變數不再擁有該資料時才會被自動刪除。這可能發生在以下情況。

  • 所有者變數超出範圍並被銷燬
  • 所有者變數被設定為另一個值,使原來的資料不再可被訪問

這就簡化了記憶體管理問題,並消除了在C或舊C++中經常發生的 "最後該由哪個指標來刪除資料 "的混亂問題。所有權不是Rust獨有的。現代C++推薦使用'unique_ptr',一個智慧指標,它也 "擁有 "它所包裹的資料,而不是原始指標。

當你在Rust中宣告一個變數時,該變數 "擁有 "資料。

let a = Box::new(2); // a "owns" a heap allocated integer


當一個變數擁有某樣東西時,它可以使用賦值將其轉移到其他變數。在讓出其資料後,舊變數不能再訪問該值,而新變數是新的所有者。

let mufasa = Box::new("king"); // mufasa is the owner of "king"
let scar = mufasa; // the data "king" is moved from mufasa to scar

println!("{}", scar); // scar is now the owner of "king"
println!("{}", mufasa); // ERROR: mufasa can no longer be accessed


在建立所有者的範圍結束時,資料被銷燬。

{
    let a = Box::new(2); // a owns a heap allocated integer
} // a's data deallocated here 


技巧和竅門
1、 將數值傳入函式會將資料 "移動 "到函式變數中。一旦發生這種情況,原來的變數就不能被訪問。這似乎很有限制性,這就是為什麼下一個話題要嘗試解決這個問題。

fn hello(a: Box<i32>) {
    println("{:?}", a); // prints "2"
}

fn main() {
    let b = Box::new(2);
    hello(b); // moves b into hello's a parameter
    
    b; // ERROR: cannot access b after it gave its value to a
}


2、在使用Rust時,一個常見的問題是,當資料被一個容器(如Vec或Option)所包圍時,如果你想把資料取出來,你必須先手動克隆資料或把它從容器中移除。一個問題是,有時你想把資料從容器中移出,而又不妨礙對容器變數的訪問。要做到這一點,你可以使用mem::replace函式,它將變數 "重置 "為某個值並返回擁有的資料。之後,原來擁有的變數不再擁有這些資料,而被設定為返回值的變數現在擁有這些資料。例如,這裡是一個連結列表的程式碼片段。

use std::mem;

type Link<T> = Option<Box<Node<T>>>;

struct Node<T> {
    data: T,
    next: Link<T>,
}

pub struct Stack<T> {
    size: i32,
    head: Link<T>,
}

impl<T> Stack<T> {
    // ... other methods

    pub fn pop(&mut self) -> Option<T> {
        let head = mem::replace(&mut self.head, None); // retrieve the Node from the Option and and set self.head to be None
        head.map(|old_head| {
            let old_head = *old_head;
            self.head = old_head.next;
            self.size -= 1;
            
            old_head.data
        })
    }
}

因為這種情況在Options中特別常見,所以有一個Options的take()方法,做同樣的事情,但不那麼冗長。

impl<T> Stack<T> {
    // ... other methods
    
    pub fn pop(&mut self) -> Option<T> {
        self.head.take().map(|old_head| { // retrieve the Node from the Option and set self.head to be None
            let old_head = *old_head;
            self.head = old_head.next; // self.head is None so you can freely set it
            self.size -= 1;

            old_head.data
        })
    }
}


借用
上一節強調了所有權本身的一個大缺陷。在很多情況下,你想運算元據,但又不真正擁有資料。例如,你可能想把一個值傳遞給一個函式,但仍然能夠在函式之外呼叫所有者變數。

Rust允許你使用借用的概念來做到這一點。借用就像你所想的那樣,它只是允許另一個變數暫時借用你的變數中的資料,並在完成後將其歸還。

Rust允許你有兩種型別的借用。

  • 帶有'&'的不可變的借用(你可以讀取借用資料的值,但你不能修改它)
  • 帶有'&mut'的可變借用(你可以讀取和修改借用的資料的值)

你既可以有
  • 有很多不可變的借用
  • 只有一個可變的借用

在任何給定的時間內,在一個作用域中的一塊資料只有一個可變的借用。所以你應該儘量在大多數時候做不可變的借用,只有在你真正需要的時候才做可變的借用。

要訪問一個借用的引用中的值,你要使用解除引用運算子 "*"。

當你借用一個變數時,所有者變數變得不可訪問,直到借用的變數被銷燬。

let mut x = 5;
let y = &mut x; // y從x那裡借用了資料。
println! ("{}", x); // ERROR: x不再擁有資料,y擁有它!

當一個借來的變數被銷燬時,它會把借來的值還給所有者。

let mut x = 5;

{ 
    let y = &mut x; // y從x那裡借來了資料。
    *y += 1; // y改變借用的資料
} // y把資料還給x

println!("{}", x); // x又拿回了資料(並且它被改為6)。

正因為如此,所有者要比借用者活得更久

let mut x: &i32;

{
    讓y = 3;
    x = &y; // ERROR: x比y活得長!
} // y在這裡被毀掉了! 如果Rust編譯器不阻止這種情況的發生,x會發生什麼?



技巧和竅門
函式引數最終大多是借來的引用,因為否則值會被移到函式內部。

函式的返回值不應該是對區域性變數的引用,Rust不會讓你這樣做。如果你在C語言中返回了一個指向區域性變數的指標,你可能會導致資料被破壞,或者返回值不知道什麼時候要去掉它的資料,不管你怎麼看,這都是不好的。

不要擔心取消引用來 "讀 "或 "寫"(取決於&或&mut)一個借用的引用的值

enum State {
    Hello,
    Bye,
}
fn hello(blah: &State, foo: &mut i32) {
    match *blah { // you are only reading an immutable reference so its fine
        State::Hello => println!("Hello!"),
        State::Bye => println!("Bye!"),
    }
    
    *foo += 1; // you are only writing to a mutable reference so its fine
}


不要擔心將解除引用的引用分配給變數,因為這將試圖將資料轉移到新的變數中。

enum State {
    Hello, 
    Bye,
}
fn hello(blah: &State) {
    let thief = *blah; // ERROR: blah can't give a borrowed item to thief! 
                       // 小偷要拿走值而不還給原主人
}

賦值並不是唯一會試圖移出借來的引用的東西。例如,模式匹配也會試圖移動值。為了防止這種情況,在匹配變數前面加上'ref'來借用匹配變數而不是移動

enum State {
    Hello(String),
    Bye,
}

fn hello(blah: &State) {
    match *blah {
        State::Hello(s) => println!("Hello, {}", s), // ERROR: moves data inside blah (the string) into the variable s
        State::Bye => println!("Bye!"),
    }
    
    // do this instead
    match *blah {
        State::Hello(ref s) => println!("Hello, {}", s), // borrow the string data inside blah
        State::Bye => println!("Bye!"),
    }
}

詳細點選標題

相關文章