Rust let mut shadowing type const

[Rust] 變數與型別

cargo run 跑得起來之後,第一個要建立的直覺是:Rust 的變數預設不可變、型別系統很嚴但會幫你推導。這篇把 let / mut / const / shadowing、純量與複合型別、函式與最基本的控制流程一次串完,後面每一篇都會反覆用到。

let 與 mut:預設不可變

Rust 的變數預設不可變(immutable),跟絕大多數命令式語言相反。

fn main() {
    let x = 5;
    x = 6; // ❌ 編譯錯誤:cannot assign twice to immutable variable `x`
}

要可變必須加 mut

fn main() {
    let mut x = 5;
    x = 6; // ✅
    println!("{x}");
}

這個設計逼你在宣告時就想清楚「這個值會不會變」。多數變數其實不需要變;看到 mut 就是一個明確的訊號:這個值之後會被改。

Shadowing:同名重新綁定

let 可以重複宣告同名變數,新變數會「遮蔽」舊變數。這跟 mut 不一樣:

fn main() {
    let x = 5;
    let x = x + 1;        // 重新綁定,x = 6
    let x = x * 2;        // 再次綁定,x = 12
    println!("{x}");      // 12

    // shadowing 還能改變型別(mut 做不到這件事)
    let spaces = "   ";          // 字串
    let spaces = spaces.len();   // 數字:原字串的長度
    println!("{spaces}");        // 3
}

mut 只能在原型別上改值;shadowing 等於開一個新變數,所以連型別都能換。.len() 回傳的數字型別後面型別段會說,這裡先當「就是個整數」即可。

常數 const

const MAX_USERS: u32 = 10_000;

差異:

letconst
預設可變性不可變(要 mut)永遠不可變
必須標註型別否(可推導)
可用範圍區塊內任何 scope,包含全域
求值時機執行期編譯期

慣例:常數用 SCREAMING_SNAKE_CASE

純量型別

整數

長度有號無號
8-biti8u8
16-biti16u16
32-biti32u32
64-biti64u64
128-biti128u128
平台相依isizeusize

整數字面值預設是 i32

let a = 42;          // i32
let b = 42u64;       // 後綴指定型別
let c: i64 = 42;     // 標註指定
let d = 1_000_000;   // 底線分隔,可讀性
let hex = 0xff;
let oct = 0o77;
let bin = 0b1010;

整數溢位行為:debug 模式下會 panic,release 模式下會 wrapping(繞回)。 要明確處理溢位,用 checked_addwrapping_addsaturating_addoverflowing_add 等方法。

let x: u8 = 250;
let y = x.checked_add(10);  // None(溢位)
let z = x.wrapping_add(10); // 4
let w = x.saturating_add(10); // 255(停在最大值)

實務上:想明確處理溢位用 checked_add(回傳 Option);想要 wrap-around 行為(例如密碼學)用 wrapping_add;想飽和到邊界值(例如計數器)用 saturating_add

浮點數

只有 f32f64。預設 f64

let x = 2.0;        // f64
let y: f32 = 3.0;

布林與字元

let flag: bool = true;
let c: char = 'z';        // char 是 4 byte,存 Unicode scalar value
let emoji: char = '😀';

字串

字串有兩種,初學常搞混:

let s1: &str = "hello";        // 字串切片,通常指向程式裡的字面值,唯讀
let s2: String = String::from("hello"); // 堆積上的可變字串
let s3: String = "hello".to_string();   // 同上,另一種寫法

&str 是借用,String 是擁有的(後面所有權那篇會講清楚)。

複合型別

Tuple

固定長度、可混型別:

let t: (i32, f64, &str) = (500, 6.4, "hi");

// 解構
let (x, y, z) = t;

// 索引
let first = t.0;
let second = t.1;

// 空 tuple,型別寫成 (),叫做 unit type
// 沒有回傳值的函式預設回傳 ()

Array

固定長度、同型別、stack 上:

let arr: [i32; 5] = [1, 2, 3, 4, 5];
let zeros = [0; 10];           // [0, 0, 0, ..., 0],長度 10
let first = arr[0];
let len = arr.len();

要可變長度的「陣列」用 Vec<T>(後面集合那篇會講)。

函式

fn add(a: i32, b: i32) -> i32 {
    a + b   // 沒分號 = 表達式 = 回傳值
}

fn greet(name: &str) {
    println!("Hello, {name}");
    // 沒寫 -> 型別,預設回傳 ()
}

Rust 區分表達式(expression)語句(statement)

  • 表達式有值(5 + 3if x > 0 { 1 } else { -1 }
  • 語句沒值,以分號結尾(let x = 5;

函式內最後一行不加分號,就是把該表達式當回傳值。加了分號就變成語句,回傳 (),會編譯錯誤。

控制流程

if 是表達式

let x = 5;
let label = if x > 0 { "positive" } else { "non-positive" };
//                    ^                    ^
//          兩個分支型別必須相同

loop / while / for

// 無窮迴圈
loop {
    println!("again");
    break;
}

// loop 可以回傳值
let result = loop {
    break 42;
};

// while
let mut n = 3;
while n > 0 {
    println!("{n}");
    n -= 1;
}

// for + range
for i in 0..5 {        // 0, 1, 2, 3, 4
    println!("{i}");
}
for i in 0..=5 {       // 0..5 包含 5
    println!("{i}");
}

// for + 集合
let arr = [10, 20, 30];
for v in arr {
    println!("{v}");
}

型別推導與標註

編譯器會盡量幫你推導型別,推不出來時就得自己標:

let x = 5;                    // ✅ 推導為 i32
let v: Vec<i32> = Vec::new(); // 必須標註,否則 Vec<?> 不確定
let v = Vec::<i32>::new();    // 或用 turbofish 語法

let s = "42".parse().unwrap();         // ❌ 不知道要 parse 成什麼
let s: i32 = "42".parse().unwrap();    // ✅
let s = "42".parse::<i32>().unwrap();  // ✅ turbofish

.parse() 是泛型方法(fn parse<F>() -> Result<F, _>),編譯器得知道你要 parse 成什麼——要嘛在變數上標 : i32,要嘛用 turbofish 寫 parse::<i32>(),兩個都沒給它就只能報錯。

變數、型別、函式與控制流程都備齊了,下一篇進 Rust 最招牌的觀念——所有權與借用。前面 String&str 埋下的「擁有」與「借用」之分,會在那裡正式說清楚。

Latest Updates

  • 2026.06.11 Content updated