Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

借用規則

本集目標

理解 Rust 的借用規則:同時只能有一個 &mut 或多個 &,以及懸垂參考的問題。

概念說明

為什麼需要規則?

上一集我們學了 &mut 可變借用。但如果 Rust 允許你同時有多個可變借用,會怎樣?

想像你有一串鑰匙圈。借給很多人&)沒問題——大家都只是看,不會改變鑰匙圈上有什麼。但如果同時借給兩個人修改&mut)——A 在加一把新鑰匙,B 同時在拆掉那把——結果就不可預測了。

這也是資料競爭(data race),會導致各種奇怪的 bug。所以 Rust 制定了嚴格的借用規則。

規則一:同時只能有一個 &mut

在同一個時間點,一個值最多只能有一個可變借用:

#![allow(unused_variables)]

fn main() {
    let mut x = 10;
    let r1 = &mut x;
    let r2 = &mut x; // 編譯錯誤!已經有一個 &mut 了
    *r1 += 1;
}

規則二:&&mut 不能同時存在

如果有人在讀(&),就不能有人在改(&mut);反過來也是:

#![allow(unused_variables)]

fn main() {
    let mut x = 10;
    let r1 = &x;     // 唯讀借用
    let r2 = &mut x; // 編譯錯誤!已經有 & 了,不能再 &mut
    println!("{}", r1);
}

規則三:多個 & 可以同時存在

多個人同時讀,沒有任何問題:

fn main() {
    let x = 10;
    let r1 = &x;
    let r2 = &x;
    let r3 = &x;
    println!("{} {} {}", r1, r2, r3); // 完全OK
}

懸垂參考(dangling reference)

還有一個重要的規則:參考不能活得比它的原值還久。一旦值離開了作用域被丟棄,任何指向它的參考就會變成懸垂參考——指向一個已經不存在的地方。Rust 在編譯時就會阻止這種事情發生。

最常見的情況是參考從內層作用域「逃」到外面:

fn main() {
    let r;
    {
        let x = 42;
        r = &x; // x 只活在這個大括號裡
    } // x 在這裡被丟棄了
    println!("{}", r); // 編譯錯誤!r 指向的 x 已經不存在了
}

x 在大括號結束時就被丟棄了,但 r 還試圖在外面使用它——Rust 不允許。

另一個常見的情況是函數試圖回傳區域變數的參考:

fn bad() -> &i32 {
    let x = 42;
    &x // x 在函數結束時就被丟棄了,參考會指向一個已經不存在的值
}

fn main() {}

道理是一樣的:x 在函數結束後就消失了,回傳的參考會指向一個不存在的值。

至於 Rust 是怎麼追蹤「參考還有沒有效」的,那就是之後會學到的生命週期(lifetime)概念了。這裡先記住:參考不能活得比它指向的值還久

範例程式碼

#[derive(Debug)]
struct Point {
    x: i32,
    y: i32,
}

fn main() {
    // 多個不可變借用:OK
    let p = Point { x: 1, y: 2 };
    let r1 = &p;
    let r2 = &p;
    println!("r1 = {:?}, r2 = {:?}", r1, r2);

    // 一個可變借用:OK
    let mut p2 = Point { x: 10, y: 20 };
    {
        let r3 = &mut p2;
        r3.x += 5;
        println!("修改後:{:?}", r3);
    } // r3 離開作用域,可變借用結束

    // 可變借用結束了,現在可以用 & 借用
    let r4 = &p2;
    println!("唯讀借用:{:?}", r4);

    // 示範:同時多個唯讀借用
    let nums = [10, 20, 30, 40, 50];
    let slice1 = &nums[0..3];
    let slice2 = &nums[2..5];
    println!("slice1 = {:?}", slice1);
    println!("slice2 = {:?}", slice2);
}

重點整理

  • 允許無限制地借用同樣會造成資料競爭,所以 Rust 制定了借用規則
  • 同一時間只能有一個 &mut,不能同時有兩個可變借用
  • &&mut 不能同時存在——要嘛大家都只讀,要嘛只有一個人在改
  • 多個 & 可以同時存在,多人同時讀沒問題
  • 懸垂參考:參考不能活得比它指向的值還久——不管是值離開了作用域,還是函數回傳區域變數的參考都不行
  • Rust 靠這些規則在編譯時就防止資料競爭,之後會學到生命週期來更精確地追蹤參考的有效期