理解Rust的變量作用域

Rust的所有權系統和作用域息息相關,因此有必要先理解Rust的作用域規則。

在Rust中,任何一個可用來包含代碼的大括號都是一個單獨的作用域。類似於Struct{}這樣用來定義數據類型的大括號,不在該討論範圍之內,本文後面所說的大括號也都不考慮這種大括號。

包括且不限於以下幾種結構中的大括號都有自己的作用域:

  • if、while等流程控制語句中的大括號
  • match模式匹配的大括號
  • 單獨的大括號
  • 函數定義的大括號
  • mod定義模塊的大括號

例如,可以單獨使用一個大括號來開啟一個作用域:


#![allow(unused)]
fn main() {
{                    // s 在這裡無效, 它尚未聲明
  let s = "hello";   // 從此處起,s是有效的
  println!("{}", s); // 使用 s
}                    // 此作用域已結束,s不再有效
}

上面的代碼中,變量s綁定了字符串字面值,在跳出作用域後,變量s失效,變量s所綁定的值會自動被銷燬。

注:上文【變量s綁定的值會被銷燬】的說法是錯誤的

實際上,變量跳出作用域失效時,會自動調用Drop Trait的drop函數來銷燬該變量綁定在內存中的數據,這裡特指銷燬堆和棧上的數據,而字符串字面量是存放在全局內存中的,它會在程序啟動到程序終止期間一直存在,不會被銷燬。可通過如下代碼驗證:

fn main(){
    {
      let s = "hello";
      println!("{:p}", s);  // 0x7ff6ce0cd3f8
    }

    let s = "hello";
    println!("{:p}", s);  // 0x7ff6ce0cd3f8
}

因此,上面的示例中只是讓變量s失效了,僅此而已,並沒有銷燬s所綁定的字符串字面量。

但一般情況下不考慮這些細節,而是照常描述為【跳出作用域時,會自動銷燬變量所綁定的值】。

任意大括號之間都可以嵌套。例如可以在函數定義的內部再定義函數,在函數內部使用單獨的大括號,在函數內部使用mod定義模塊,等等。

fn main(){
  fn ff(){
    println!("hello world");
  }
  ff();

  let mut a = 33;
  {
    a += 1;
  }
  println!("{}", a);  // 34
}

雖然任何一種大括號都有自己的作用域,但函數作用域比較特別。函數作用域內,無法訪問函數外部的變量,而其他大括號的作用域,可以訪問大括號外部的變量

fn main() {
  let x = 32;
  fn f(){
    // 編譯錯誤,不能訪問函數外面的變量x和y
    // println!("{}, {}", x, y);  
  }
  let y = 33;
  f();

  let mut a = 33;
  {
    // 可以訪問大括號外面的變量a
    a += 1;
  }
  println!("{}", a);
}

在Rust中,能否訪問外部變量稱為【捕獲環境】。比如函數是不能捕獲環境的,而大括號可以捕獲環境。

對於可捕獲環境的大括號作用域,要注意Rust的變量遮蓋行為。

分析下面的代碼:

fn main(){
  let mut a = 33;
  {
    a += 1;   // 訪問並修改的是外部變量a的值

    // 又聲明變量a,這會發生變量遮蓋現象
    // 從此開始,大括號內訪問的變量a都是該變量
    let mut a = 44; 
    a += 2;
    println!("{}", a);  // 輸出46
  }    // 大括號內聲明的變量a失效
  println!("{}", a);   // 輸出34
}

這種行為和其他語言不太一樣,因此這種行為需要引起注意。

懸垂引用

在支持指針操作的語言中,一不小心就會因為釋放內存而導致指向該數據的指針變成懸垂指針(dangling pointer)。

Rust的編譯器保證永遠不會出現懸垂引用:引用必須總是有效。即引用必須在數據被銷燬之前先失效,而不能銷燬數據後仍繼續持有該數據的引用。

例如,下面的代碼不會通過編譯:

fn main(){
  let sf = f();  // f()返回值是一個無效引用
}

fn f() -> &String {
  let s = String::from("hello");
  &s  // 返回s的引用
}   // s跳出作用域,堆中String字符串被釋放

該示例報錯的原因很明顯,函數的返回值&s是一個指向堆中字符串數據的引用(注意,引用是一個實實在在的數據),當函數結束後,s跳出作用域,其保存的字符串數據被銷燬,這使得返回值&s變成了一個無效的引用。

這裡的懸垂指針非常明顯,但很多時候會在非常隱晦的情況下導致懸垂指針,幸好Rust保證了絕不出現懸垂指針的問題。