理解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保證了絕不出現懸垂指針的問題。