變數與可變性

如同「透過變數儲存數值」提到的,變數預設是不可變的。這是 Rust 推動你能充分利用 Rust 提供的安全性和簡易並行性來寫程式的許多方法之一。不過,你還是有辦法能讓你的變數成為可變的。讓我們來探討為何 Rust 鼓勵你多多使用不可變,以及何時你會想要改為可變的。

當一個變數是不可變的,只要有數值綁定在一個名字上,你就無法改變其值。為了方便說明,讓我們使用 cargo new variablesprojects 目錄下產生一個新專案叫做 variables

再來在你的 variables 目錄下開啟 src/main.rs 然後覆蓋程式碼為以下內容。這是段還無法編譯的程式碼:

檔案名稱:src/main.rs

fn main() {
    let x = 5;
    println!("x 的數值為:{x}");
    x = 6;
    println!("x 的數值為:{x}");
}

儲存然後使用 cargo run 執行程式。你應該會收到一則有關於不可變的錯誤訊息,如下所示:

$ cargo run
   Compiling variables v0.1.0 (file:///projects/variables)
error[E0384]: cannot assign twice to immutable variable `x`
 --> src/main.rs:4:5
  |
2 |     let x = 5;
  |         -
  |         |
  |         first assignment to `x`
  |         help: consider making this binding mutable: `mut x`
3 |     println!("x 的數值為:{x}");
4 |     x = 6;
  |     ^^^^^ cannot assign twice to immutable variable

For more information about this error, try `rustc --explain E0384`.
error: could not compile `variables` due to previous error

此範例顯示了編譯器如何協助你找到你程式碼的錯誤。雖然看到編譯器錯誤訊息總是令人感到沮喪,但這通常是為了讓你知道你的程式無法安全地完成你想讓它完成的任務。它們不代表你不是個優秀的程式設計師!有經驗的 Rustaceans 時常會與編譯器錯誤訊息打交道。

你得到的錯誤訊息說明「cannot assign twice to immutable variable x」,因為你嘗試第二次賦值給 x 變數。

當我們嘗試改變一個原先設計為不可變的變數時,能夠產生編譯時錯誤是很重要的。因為這樣的情況很容易導致程式錯誤。如果我們有一部分的程式碼在執行時認為某個數值絕對不會改變,但另一部分的程式碼卻更改了其值,那麼這就有可能讓前一部分的程式碼就可能以無法預測的方式運行。這樣的程式錯誤的起因是很難追蹤的,尤其是當第二部分的程式碼偶而才會改變其值。Rust 編譯器會保證當你宣告一個數值不會被改變時,它就絕對不會被改變。這樣你就不需要去追蹤該值可能會被改變,讓你的程式碼更容易推導。

但同時可變性也是非常有用的,能讓程式碼變得更好寫。雖然變數預設是不可變的,但就如同第二章一樣你可以在變數名稱前面加上 mut 讓它們可以成為可變的。加上 mut 也向未來的讀取者表明了其他部分的程式碼將會改變此變數的數值。

舉例來說,讓我們改變 src/main.rs 成以下程式碼:

檔案內容:src/main.rs

fn main() {
    let mut x = 5;
    println!("x 的數值為:{x}");
    x = 6;
    println!("x 的數值為:{x}");
}

當你執行程式的話,我們會得到:

$ cargo run
   Compiling variables v0.1.0 (file:///projects/variables)
    Finished dev [unoptimized + debuginfo] target(s) in 0.30s
     Running `target/debug/variables`
x 的數值為:5
x 的數值為:6

當使用 mut 時,我們可以將 x 的數值從 5 改變為 6。何時使用可變性的決定權在你手上,你可以依照特定場合做出你認為最佳的選擇。

常數

常數(constants)。和不可變變數一樣,常數會讓數值與名稱綁定且不允許被改變,但是不可變變數與常數還是有些差異。

首先,你無法在常數使用 mut,常數不是預設不可變,它們永遠都不可變。如果你使用 const 宣告而非 let 的話,你必須指明型別。我們會在下一章「資料型別」詳細解釋型別與型別詮釋,所以現在先別擔心細節。你只需要先知道你永遠必須先詮釋常數的型別。

常數可以被定義在任一有效範圍,包含全域有效範圍。這讓它們非常有用,讓許多部分的程式碼都能夠知道它們。

最後一個差別是常數只能被常數表達式設置,而不能用任一在運行時產生的其他數值設置。

以下為一個常數名稱的範例:

#![allow(unused)]
fn main() {
const THREE_HOURS_IN_SECONDS: u32 = 60 * 60 * 3;
}

變數的名稱為 THREE_HOURS_IN_SECONDS ,且它的數值被設為 60(一分鐘有多少秒)乘上 60(一小時有多少分鐘)乘上 3(此程式想要計算的小時數量)。Rust 的常數命名規則為使用全部英文大寫並用底線區隔每個單字。編譯器能夠在編譯時用特定限制集合內的操作進行運算,讓我們能用易於理解且驗證的方式寫出此數值,而不用將常數設爲 10,800。你可以查閱 Rust Reference 的 constant evaluation 段落來瞭解哪些操作可以在宣告常數時使用。

在整支程式運行時,常數在它們的範圍內都是有效的。這樣的性質讓常數在處理應用程式中需要被許多程式碼部份所知道的數值的情況下是非常好的選擇,像是一款遊戲中玩家能夠得到的最高分數或者光速的數值。

將會擴散到所有程式碼的數值定義為常數,對於幫助未來程式碼的維護者理解是非常好的選擇。這也讓未來需要更新數值的話,你知道需要修改寫死的地方就好。

遮蔽(Shadowing)

如同你在猜謎遊戲教學所看到的,在第二章你可以用之前的變數再次宣告新的變數。Rustaceans 會說第一個變數被第二個變數所遮蔽了,這代表當你使用該變數名稱時,編譯器會看到的是第二個變數的數值。第二個變數會遮蔽第一個變數,佔據變數名稱的使用權,直到它自己也被遮蔽或是離開作用域。我們可以用 let 關鍵字來重複宣告相同的變數名稱來遮蔽一個變數:

檔案名稱:src/main.rs

fn main() {
    let x = 5;

    let x = x + 1;

    {
        let x = x * 2;
        println!("x 在內部範圍的數值為:{x}");
    }

    println!("x 的數值為:{x}");
}

此程式首先將 x 給予 5,然後它重複用 let x = 建立一個新變數 x,取代了原本的數值並加上 1,所以 x 的數值變為 6。然後在接下來括號的內部範圍內,第三次的 let 陳述式一樣遮蔽了 x 讓它將原本的值乘與 2,讓 x 數值為 12。當該範圍結束時,內部的遮蔽也結束,所以 x 就回到原本的 6。當我們運行此程式時,就會輸出以下結果:

$ cargo run
   Compiling variables v0.1.0 (file:///projects/variables)
    Finished dev [unoptimized + debuginfo] target(s) in 0.31s
     Running `target/debug/variables`
x 在內部範圍的數值為:12
x 的數值為:6

遮蔽與標記變數為 mut 是不一樣的,因為如果我們不小心重新賦值而沒有加上 let 關鍵字的話,是會產生編譯期錯誤的。使用 let 的話,我們可以作出一些改變,然後在這之後該變數仍然是不可變的。

另一個 mut 與遮蔽不同的地方是,我們能有效地再次運用 let 產生新的變數,可以在重新運用相同名稱時改變它的型別。舉例來說,當我們希望程式要求使用者顯示出字串間應該顯示多少空格,同時我們又希望它被存為一個數字時,我們可以這樣做:

fn main() {
    let spaces = "   ";
    let spaces = spaces.len();
}

第一次宣告 spaces 的變數是一個字串型別,而第二次宣告 spaces 則成了數字型別。遮蔽這項功能讓我們不必去宣告像是 spaces_strspaces_num,我們可以重複使用 spaces 這個變數名稱。不過,可變變數仍然是無法變更變數型別的,如果這樣做的話我們就會拿到編譯期錯誤:

fn main() {
    let mut spaces = "   ";
    spaces = spaces.len();
}

此錯誤訊息告訴我們我們不允許改變變數的型別:

$ cargo run
   Compiling variables v0.1.0 (file:///projects/variables)
error[E0308]: mismatched types
 --> src/main.rs:3:14
  |
2 |     let mut spaces = "   ";
  |                      ----- expected due to this value
3 |     spaces = spaces.len();
  |              ^^^^^^^^^^^^ expected `&str`, found `usize`

For more information about this error, try `rustc --explain E0308`.
error: could not compile `variables` due to previous error

現在我們講完變數了,讓我們看看它們可以擁有的資料型別吧。