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;
差異:
let | const | |
|---|---|---|
| 預設可變性 | 不可變(要 mut) | 永遠不可變 |
| 必須標註型別 | 否(可推導) | 是 |
| 可用範圍 | 區塊內 | 任何 scope,包含全域 |
| 求值時機 | 執行期 | 編譯期 |
慣例:常數用 SCREAMING_SNAKE_CASE。
純量型別
整數
| 長度 | 有號 | 無號 |
|---|---|---|
| 8-bit | i8 | u8 |
| 16-bit | i16 | u16 |
| 32-bit | i32 | u32 |
| 64-bit | i64 | u64 |
| 128-bit | i128 | u128 |
| 平台相依 | isize | usize |
整數字面值預設是 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_add、wrapping_add、saturating_add、overflowing_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。
浮點數
只有 f32 與 f64。預設 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 + 3、if 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
