變量與作用域
變量的聲明與初始化
Rust的基本語法格式如下:
fn main(){let bunnies = 2;
}
語句以分號結尾,用花括號包含語句塊。 Rust的語法其實借鑒了很多其他的語言,比如C語言和Python, 所以變量定義的格式看起來也跟很多我們熟悉的其他語言相似。Rust中,使用let
關鍵字聲明一個變量。在上面的例子中, 我們聲明了一個變量bunnies
, 并且初始化了它的值為2
.
Rust是一種強類型的語言,那么在上面的語句中,哪里標注了這個變量的類型呢?在Rust編程中,如果Rust能準確的識別這個變量的類型,那么我們不需要顯式的標注變量的類型,也不需要像C#那樣標注一個auto
表示它的類型是自動識別的。
如果需要顯式的標注一個變量的類型,可以像下面的例子一樣做, 在變量名后加個:
, 后面再寫上變量的類型,如下,i32
代表有符號的32位整型。
fn main(){let bunnies: i32 = 2;
}
與python類似,rust也可以在一行語句中定義多個變量。如下例子便可以在一行代碼中為兩個變量初始化:
fn main(){let (bunnies, carrots) = (8, 50);
}
變量不可變
在Rust中,變量默認其實是不可變的,也就是說,一旦對一個變量賦值以后,其值默認是不可被修改的。這一特點與大多數的其他編程語言都不同,其他編程語言的變量默認是隨時可以被重新賦值的。那為什么Rust要將變量設置為默認不可變的呢?這就要提到上一章中我們提到的Rust的三個特性了:
- 內存安全: 如果變量在運行過程中始終不變,這可以避免很多bug的發生,變量不可變這一設計,極大的提高了Rust的內存安全特性。
- 無畏并發: 不變的變量,可以被多個線程在不加鎖的情況下共享,這也使得Rust的并發更安全可靠。
- 高性能: 不變的變量,使得編譯器可以對其進行額外的優化,從而提高了代碼的執行速度,提高了程序運行性能。
但是不得不承認我們在編程中一定會遇到需要修改變量的需求, 如果我們直接修改變量的值,編譯便會報錯,例如下面的代碼:
fn main(){let bunnies: i32 = 2;bunnies = 3; // Error!
}
如果運行上面的代碼,將會得到下面的報錯,可以看到,報錯中非常明確的指出了代碼的問題所在,并且還指出了修改建議, 在報錯的最上面,給出了錯誤的描述,也就是:不能對不可變變量進行二次賦值。在報錯中,也指出了錯誤所在的位置,第3行第5列。接下來還對整個錯誤的上下文進行了說明,告訴我們在第2行的時候對變量bunnies
已經賦值,然后再第3行再次對不可變變量bunnies
進行了賦值,因此報錯。接著還提出了修改建議,讓我們在第2行的變量名前面加上mut
, 使其成為一個可變變量,也許能修復這個問題。在最后一行,如果上面的提示還不能解決問題,還可以運行rustc --explain e0384
來查看錯誤的完整描述。
按照錯誤提示,我們將代碼修改后如下便可以成功運行了:
fn main(){let mut bunnies: i32 = 2;bunnies = 3; // Error!
}
常量
在Rust中,常量(constant)其實也屬于變量的一種, 相比普通的不可變變量,它更加的不可變。定義一個常量包含以下四個關鍵步驟:
- 以
const
而不是let
聲明; - 變量名格式為全大寫字母加下劃線分隔;
- 必須聲明變量類型;
- 常量的值必須時編譯時可確定值的表達式;
下面是普通變量和常量聲明的對比:
let wrap_factor = ask_scotty(); // 變量
const WRAP_FACTOR: f64 = 9.9; // 常量
定義一個常量比變量麻煩很多,那為什么還要用常量呢?
- 常量可以在函數作用域外或者模塊外進行定義,而在任意的地方使用;
- 常量會在編譯時被靜態的寫入可執行文件,使得運行速度很快;
- Rust官方在每個發布版本中都對const類型增加了越來越多的功能和優化,在可以使用const的地方,使用const是一個好的選擇;
作用域
每個變量都有各自的作用域,只有在變量的作用域中,變量才能被使用。代碼的作用域通常是從變量被創建的地方開始,到變量所在的代碼塊結束, 在這個范圍中的子代碼塊中,變量仍然是可以被訪問的。
注: 代碼塊是一組被花括號包含的語句
fn main() {let x = 5;{let y = 99;println!("x = {}, y = {}", x, y);}println!("x = {}, y = {}", x, y); // Error!
}
在上面的代碼中, 變量x
在main
函數的代碼塊中被定義,其中定義了一個子代碼塊,在子代碼塊中定義了一個變量y
, 在子代碼塊中,x
和 y
都可以被訪問, 在子代碼塊結束時, y
立刻被銷毀(Rust中沒有任何的垃圾回收器,變量總是在離開作用域后被立即銷毀),因此第二個println!
語句不能訪問變量y
而發生錯誤。
然而我們不用擔心這會在運行時發生bug, 因為這種錯誤會在編譯時就被暴露出來。
變量隱藏
Rust中,也存在變量隱藏的現象
fn main() {let x = 5;{let x = 99;println!("x = {}", x);}println!("x = {}", x); // Error!
}
運行結果應該如下:
x = 99
x = 5
在上述代碼中我們在子代碼塊外部定義了一個變量x
并賦值為5
, 在子代碼塊中,x
的值被覆蓋,為內層代碼塊中的值99
。當離開了內層代碼塊后,內層的變量x
被銷毀, x
的值又變回了外層代碼塊中的5
.
再來看一個例子:
fn main() {let mut x = 5; // x is mutablelet x = x; // x is now immutable
}
這個例子中,第一個x
被隱藏了,這其實相當于重新聲明并初始化了x
這個變量,在編譯過程中, Rust甚至能識別到這種情形并優化執行的過程,并不會真的先定義一個可變的x
, 再用一個新的x
去覆蓋它,而是直接定義一個不可變的變量x
并為其賦值為5
.
再看一個例子:
fn main() {let meme = "More cowbell!";let meme = make_image(meme);
}
在上述代碼中, 變量meme
甚至能被改變類型(從字符串變成了圖片)。
變量與內存安全
在Rust中,在使用一個變量前,必須確保這個變量被初始化。
情景A
fn main() {let enigma: i32;println!("{}", enigma); // Error!
}
可以看到,報錯提示我們,變量雖然被聲明了,但是沒有被初始化。
情景B
fn main() {let enigma: i32;if true{enigma = 42;}println!("{}", enigma); // Error!
}
即時是在一個恒為真的判斷語句中為變量進行了初始化,編譯器仍會報錯, 因為判斷語句只有在運行時才能被判別最終的結果,因此在編譯時沒辦法確保該變量一定會被初始化。
為了保證變量一定被初始化,可以將上述代碼改為如下:
fn main() {let enigma: i32;if true{enigma = 42;} else {enigma = 7;}println!("{}", enigma); // Error!
}
如果在C語言中使用了一個未初始化的變量會出現什么現象呢,如下代碼:
include <stdio.h>
int main(){int enigma;printf("%d\n", enigma);
}
這將不會導致編譯報錯,程序可以正常運行,但是會輸出一個不可預測的結果,因為聲明變量后, C語言就會在內存分配一個地址,而這個內存地址中存儲的是什么數據,我們不得而知,它可能是任何東西。
小結
本章介紹了Rust中變量的種類,聲明與賦值方式,以及變量的作用域和隱藏特性。下一章將介紹Rust的函數及模塊系統。