[為你自己學 Rust] 變數與常數

[為你自己學 Rust] 變數與常數

變數與常數

到這個章節才在介紹變數(Variable)與常數(Constant)似乎有點晚, 其實前面的章節已經在用了。如同其它程式語言的設計,Rust 也有變數跟常數的設計,不過 Rust 的變數有一些比較特別的地方,這也是 Rust 會被說比較「安全」的原因。

在 Rust 可以使用 let 關鍵字定義變數:

let age: u8 = 20;

在前面章節介紹過型別,Rust 的編譯器需要知道每個變數的型別,所以要不你在撰寫的時候就講明白,或是讓 Rust 幫你猜。別擔心,Rust 的編譯器還滿聰明的,大部份時候都會猜對,只是偶爾會猜的寬鬆一點,例如沒指定型別的整數變數會是 i32,而沒指定型別的浮點數會是 f64

你也可以先宣告變數但不給值,之後再給也行:

let age;
age = 20;

println!("{}", age);  // 印出 20

不過如果沒給它值,是不能直接拿來用的:

let age: u8;    // 沒給值
println!("{}", age);  

這執行下去就會出錯了:

$ cargo run
error[E0381]: used binding `age` isn't initialized
  |
2 |     let age: u8;
  |         --- binding declared here but left uninitialized
3 |     println!("{}", age);
  |                    ^^^ `age` used here but it isn't initialized

要拿來用之前得先給定一個值,不然 Rust 可不會像 JavaScript 那麼客氣就只給你個 undefined 就沒事了。

變數不能變?

在大部份的程式語言,變數就是可以「變」才叫變數,但在 Rust 的設計裡,let 宣告的變數是不能改的:

let age = 20;
println!("{}", age);

age = 18;    // 要把它改成 18
println!("{}", age);

Rust 就會給你這個錯誤訊息:

$ cargo run
error[E0384]: cannot assign twice to immutable variable `age`
  |
2 |     let age = 20;
  |         ---
  |         |
  |         first assignment to `age`
  |         help: consider making this binding mutable: `mut age`
...
5 |     age = 18;
  |     ^^^^^^^^ cannot assign twice to immutable variable

Rust 宣告的變數預設是不可變動(immutable),所以在給定值之後不能修改。如果要讓它可被修改,需要在宣告的時候多加一個形容詞 mut ,跟 Rust 說這是可以修改的:

let mut age = 20;    // 加上了 mut 修飾
println!("{}", age);

age = 18;
println!("{}", age);

這樣用起來就跟其它程式語言的變數差不多像了。

變數預設是不可修改的有什麼好處?好處就是它不會因為不小心被改到而發生神奇的錯誤,這是刻意的設計,只有在必要的時候才加上 mut 宣告,這麼一來你會很清楚的知道這個變數是需要被變動的。一個好的開發者應該從最基本的地方就應該要訓練自己知道每個變數的用途以及需不需要改,這是很好的練習。

不要為了怕麻煩或貪圖一時便利,有 mut 可以用就每個都 mut 下去,這樣就辜負了 Rust 給你 mut 的原意了,這樣一來天生再安全的程式語言也會被你寫的很不安全。如果你宣告變數的時候真的這樣做:

let mut age = 20;     // 宣告了 mut 但後面沒有真的改
println!("{}", age);

說要 mut 但後來沒有真的更改的話,程式執行是不會錯啦,但 Rust 編譯器就又會出來抱怨了:

$ cargo run
warning: variable does not need to be mutable
  |
2 |     let mut age = 20;
  |         ----^^^
  |         |
  |         help: remove this `mut`

你想的到偷吃步 Rust 編譯器也想的到,所以 Rust 請你把這個 mut 拿掉。

作用域(Scope)

scope 是指變數在程式碼中可見的範圍,這個在其它程式語言裡也都有相同的概念,以底下這個例子來說:

fn main() {
    let a = 10;

    if true {
        println!("{}", a);  // 這個 block 裡面沒有變數 a,所以找到外面的 a
    }

    println!("{}", a);
}

if 區塊裡試著想要印出變數 a,但在這個區塊裡並沒有這個變數,Rust 會試著找外面一層,然後就會找到 10;相對的,如果該區塊裡面有該變數的存在:

fn main() {
    let a = 10;

    if true {
        let a = 20;
        println!("{}", a);  // 在 block 裡有變數 a,所以印出 20
    }

    println!("{}", a);  // 不會受 if 裡的宣告所影響
}

就會取用該區塊裡的變數,而且不會影響到外層的同名變數。Rust 的變數在離開 block 之後就無法再使用,所以如果這樣寫:

fn main() {
    if true {
        let a = 20;
    }

    println!("{}", a);
}

會得到這個結果:

$ cargo run
error[E0425]: cannot find value `a` in this scope
  |
6 |     println!("{}", a);
  |                    ^
  |
help: the binding `a` is available in a different scope in the same function
  |
3 |         let a = 20;

仔細看就會發現除了上半段的錯誤訊息外,下半段的訊息看起來有猜到你想要做的事,Rust 的編譯器是真的滿厲害的。

整體來說,let 變數的作用域的設計跟 JavaScript 在 ES6 之後推出的 let 宣告差不多。看到這裡,預設不能修改的 let 變數好像跟常數一樣,那麼還需要常數嗎?

常數(Constant)

在 Rust 宣告常數是使用 const 關鍵字,不過跟 let 宣告變數不同的是,常數沒有 mut 的設計,也就是說,常數一開始就一定要給定值,而且 Rust 還會要求你把型別講清楚:

const a = 10;

Rust 不會幫常數推斷型別,所以你得明明白白的講清楚,不然 Rust 會給你以下的錯誤訊息:

$ cargo run
error: missing type for `const` item
  |
2 |     const a = 10;
  |            ^ help: provide a type for the constant: `: i32`

還是得再次稱讚一下 Rust 編譯器給的錯誤訊息,囉嗦歸囉嗦,但給的方向滿清楚的。不只這樣,Rust 的編譯器對常數的命名方式也會管,例如宣告一個常數 my_age

const my_age: u8 = 10;
println!("{}", my_age);

程式還是可以執行,但 Rust 會給你警告訊息:

$ cargo run
warning: constant `my_age` should have an upper case name
  |
2 |     const my_age: u8 = 10;
  |           ^^^^^^ help: convert the identifier to upper case: `MY_AGE`

Rust 希望你在命名常數的時候使用全大寫英文,必要的時候用底線 _ 分隔。管很多我知道,但我覺得滿好的,有人管總比沒人管要來的好,一樣是那句老話,人治不行,就交給法治吧。

簡單的列舉幾點常數跟變數的差別:

看到這裡,是不是覺得其實 Rust 好像沒特別難,就只是要加上型別宣告會囉嗦一點而已。別擔心,再過兩個章節等介紹到「所有權(Ownership)」的時候你就會明顯感受到爬坡感了。這裡先劇透一點點給大家看看:

fn main() {
    let a = String::from("hello world");
    let b = a;

    println!("{}", b);
    println!("{}", a);
}

在上面的例子中,變數 a 是一個字串,變數 b 則是把變數 a 的值指定給它,然後分別把這兩個變數印出來。看起來很正常,執行就會發生錯誤訊息,而且錯誤訊息還有點莫名其妙:

$ cargo run
error[E0382]: borrow of moved value: `a`
  |
2 |     let a = String::from("hello world");
  |         - move occurs because `a` has type `String`, which does not implement the `Copy` trait
3 |     let b = a;
  |             - value moved here
...
6 |     println!("{}", a);
  |                    ^ value borrowed here after move

蛤?什麼移動(move)?又借(borrow)了什麼東西?不急,讓我們慢慢往下看 :)