Rust入門系列之引用和借用

木木劍光發表於2022-02-10

1. 引用

很多場景中,我們可能只是想讀取某個變數指向的值,並不想獲得其所有權,這個時候就可以使用引用。其實在很多其他的程式語言中,也有引用的概念。

  • 簡單來講,引用是建立一個變數,指向另個指標的地址,而不是直接指向 該指標指向的堆記憶體地址
  • 通過 & 取地址符獲取對一個指標變數的引用

例如在 Rust 中,我們這樣建立引用:

let s1 = String::from("hello");

// 獲取 s1 的引用
let s = &s1;
  • 下圖就很好的表示了 引用 的關係

    • 變數s1是棧記憶體中的一個指標地址,通過 ptr 記錄了儲存於堆記憶體中的 String("hello") 的地址
    • 變數s也存在棧記憶體中,通過 ptr 記錄了 s1 的指標地址,來實現對 String("hello") 的引用

截圖2022-02-04 上午11.29.19.png

2. 借用

使用引用作為函式引數的行為被稱作借用,如何使用借用來規避某個變數的所有權發生移動,我們可以看以下例子:

fn main() {
    let s = String::from("hello!");

    // 使用 s 的引用作為入參,s 的所有權就不會發生移動
    let len = get_length(&s);

    println!("the length of {} is {}", s, len); // the length of hello! is 6
}

fn get_length(string: &String) -> usize {
    // 引用和變數一樣,預設也是不可變的
    // string.push_str("world"); // `string` is a `&` reference, so the data it refers to cannot be borrowed as mutable

    string.len()
}

3.可變引用

從上面的例子我們可以知道,引用和變數一樣,預設也是不可變的,我們不能修改引用所指向的值。但如果想要這麼做,那麼可以使用 mut 關鍵字,將引用轉為可變的:

fn main() {
    let mut s = String::from("hello!");

    /*
     * 使用 mut 關鍵字,將 s 的可變引用作為入參,
     * 這樣 s 的所有權既不會發生移動,函式中也能通過 可變引用 來修改 s 的值
     */
    let len = get_length(&mut s);
    
    // 在 get_length 函式中我們實現了對 s 的修改
    println!("the length of {} is {}", s, len); // the length of hello!world is 11
}

fn get_length(string: &mut String) -> usize {
    // 通過可變引用對值進行修改
    string.push_str("world");
    string.len()
}

3.1 可變引用的重要限制

  • 對應一個指標變數,在一個特定的作用域內,只能有一個可變引用。原因也很好理解,如果在一塊作用域內,當一個變數存在兩個可變引用,那就意味著同一時間可能有兩個變數控制著同一塊記憶體空間,就會發生資料競爭,很容易在執行時產生bug,因此 Rust 通過編譯時檢查,來規避這樣的問題出現

3.1.1 同一作用域,只能存在一個可變引用

通過例子可以看到,當我們對 變數origin 進行了兩次可變引用,編譯時就直接報錯

fn main() {
    let mut origin = String::from("hello");

    let ref1 = &mut origin;
    let ref2 = &mut origin; // error: cannot borrow `origin` as mutable more than once at a time

    println!("{}, {}", ref1, ref2);
}

3.1.2 不可變引用可以有多個

如果是同時使用多個不可變引用,則不會有這個限制,因為不可變引用其實就是隻讀的,不存在可能的記憶體安全風險。通過這個例子我們可以看到,Rust是允許這樣使用的:

fn main() {
    let origin = String::from("hello");

    let ref1 = &origin;
    let ref2 = &origin;

    println!("{}, {}", ref1, ref2);
}

3.1.3 某些場景下,可以存在多個可變引用

從上面的兩個例子我們已經知道,如果一個作用域內同時存在兩個以上的可變引用,那麼就可能發生資料競爭,那麼是否存在某些場景,會出現多個可變引用呢?我們看下面這個例子:

  • 其實在程式執行過程中,只要走出作用域,那麼作用域中的變數就會被釋放,這個例子中當宣告 ref2 時,ref1 已經被銷燬了,所以還是可以保證可變引用的第一條原則

    fn main() {
      let mut origin = String::from("hello");
    
      {
          let ref1 = &mut origin;
          
          println!("{}", ref1);
      }
    
      // 當 ref2 宣告時,ref1 已經被銷燬了
      let ref2 = &mut origin;
    
      println!("{}", ref2);
    }

3.1.4 不能同時擁有一個可變引用和一個不可變引用

對於一個指標變數,它的可變引用和不可變引用其實是互斥的,不能同時存在。原因很簡單,可變引用可以修改指向記憶體空間的值,當值被修改,不可變引用的意義也就不存在了。因此 Rust 在編譯時會進行檢查,發現這種情況則會直接報錯:

fn main() {
    let mut origin = String::from("hello");

    let ref1 = &origin;
    let ref2 = &mut origin;// cannot borrow `origin` as mutable because it is also borrowed as immutable

    println!("{} {}", ref1, ref2);
}

4. 懸垂引用

這一種出現在 C/C++ 等語言中的 bug 場景,描述的是這樣一種場景,一個變數所指向的記憶體空間已經被釋放或分配給其他程式使用,但這個變數任然有效。在 Rust 中,編譯器會檢查這種場景,來避免出現懸垂引用。假設編譯通過,下面就是一個會產生懸垂引用的場景:

fn main() {
    let s = String::from("haha");

    // s 的所有權被移動到 helper 的作用域中
    let a = helper(s);

    /*
     * 假設編譯通過:
     * 當 helper 呼叫完畢,s 指向的記憶體空間就被釋放了,但在 helper 中返回了 s 的引用,
     * 其實這個引用已經失效,這時 變數a 就變為了一個 懸垂引用
     */
}

fn helper(s: String) -> &String {
    println!("{}", s);
    &s // error: missing lifetime specifier
}

但其實在編譯時,Rust就不會允許 helper 返回 s 的引用

相關文章