Rust 基本語法與變數 ── 變數宣告(let、可變性 mut)
簡介
在任何程式語言的學習曲線上,變數的宣告與使用往往是最先接觸的概念。Rust 以安全、零成本抽象聞名,而變數的不可變性(immutability)正是它防止資料競爭、提升執行效率的核心機制。掌握 let 與 mut 的語法與行為,不僅能寫出符合所有權規則的程式,也能在實務開發中避免常見的錯誤與效能陷阱。
本篇文章將從 基本語法、可變性、遮蔽(shadowing) 等概念逐一說明,並提供多個實用範例,讓你在寫 Rust 程式時能夠自信地管理資料的生命週期與可變性。
核心概念
1. let:宣告不可變變數
在 Rust 中,預設的變數是 不可變(immutable)的。這意味著一旦賦值後,變數的內容就不能再被改變,編譯器會在編譯階段直接拒絕嘗試修改的程式碼。
fn main() {
// 宣告不可變變數 x,型別會被編譯器自動推斷為 i32
let x = 10;
println!("x = {}", x);
// x = 20; // <- 編譯錯誤:cannot assign twice to immutable variable `x`
}
為什麼預設不可變?
- 防止意外修改資料,提升程式的可預測性。
- 讓編譯器在最佳化時可以假設變數不會變動,產生更快的機器碼。
2. mut:讓變數可變
若真的需要在程式執行過程中改變變數的值,必須在宣告時加上 mut 關鍵字,明確告訴編譯器「這個變數是可變的」。
fn main() {
let mut counter = 0; // 可變變數
for _ in 0..5 {
counter += 1; // 合法的賦值
}
println!("counter = {}", counter); // 輸出 5
}
注意:
mut只影響變數本身的綁定(binding),而不會改變其所指向資料的所有權或借用規則。
3. 變數遮蔽(Shadowing)
Rust 允許在相同作用域內使用相同的變數名稱重新宣告,稱為 遮蔽。遮蔽不同於 mut,因為它會產生一個全新的變數,舊的變數則被「隱藏」且在此之後不再可見。
fn main() {
let x = 5; // 第一次宣告,x 為 i32
let x = x + 1; // 產生新變數 x,值為 6
let x = x * 2; // 再產生新變數 x,值為 12
println!("最終的 x = {}", x); // 輸出 12
}
遮蔽常用於類型轉換或暫時改變變數的可變性:
fn main() {
let spaces = " "; // &str
let spaces = spaces.len(); // 重新宣告為 usize
println!("字串長度 = {}", spaces);
}
4. 型別註記與推斷
雖然 Rust 具備強大的型別推斷能力,但在某些情況下(例如需要明確的浮點型別或大整數),仍建議使用型別註記。
fn main() {
// 明確指定為 f64
let pi: f64 = 3.1415926535;
// 推斷為 i32
let answer = 42;
println!("pi = {}, answer = {}", pi, answer);
}
5. 常量(const)與靜態變數(static)
常量 與 靜態變數 與 let 的差別在於它們在編譯期就已確定值,且全域有效。const 必須在宣告時即給定值,且不可使用 mut;static 則可以是可變的,但必須透過 unsafe 存取。
const MAX_POINTS: u32 = 100_000;
static GREETING: &str = "Hello, Rust!";
程式碼範例
範例 1:基本不可變與可變宣告
fn main() {
// 不可變變數
let name = "Alice";
println!("Hello, {}", name);
// 可變變數
let mut score = 0;
score += 10;
println!("Score = {}", score);
}
重點:
name只能讀取,若嘗試name = "Bob"會編譯失敗;score因加上mut,才能在後續程式碼中改變。
範例 2:遮蔽與型別轉換
fn main() {
let value = "42"; // &str
let value = value.parse::<i32>().unwrap(); // 轉成 i32
let value = value as f64 * 1.5; // 再轉成 f64 並做運算
println!("最終結果 = {}", value); // 63.0
}
說明:每一次
let都產生新變數,舊的value被隱藏,讓程式碼在同一作用域內完成多步轉換而不需要額外的變數名稱。
範例 3:可變性與所有權的互動
fn main() {
let mut s = String::from("Rust");
// s 仍然擁有所有權,我們可以修改它的內容
s.push_str(" Programming");
println!("{}", s); // Rust Programming
}
關鍵:
String是堆積(heap)資料,只有在變數本身是mut時,才能呼叫會改變內部緩衝區的 API(如push_str)。
範例 4:在迴圈中使用可變變數
fn main() {
let mut total = 0;
for i in 1..=5 {
total += i; // 每次迭代都累加
}
println!("1+2+3+4+5 = {}", total); // 15
}
技巧:若只需要一次性的累加,亦可使用
Iterator::sum(),但手動累加有助於理解mut的實際作用。
範例 5:結合 const 與 mut
const MAX: i32 = 10;
fn main() {
let mut counter = 0;
while counter < MAX {
println!("counter = {}", counter);
counter += 1;
}
}
說明:
MAX為編譯期常量,無法被改變;counter為可變變數,用於控制迴圈。
常見陷阱與最佳實踐
| 陷阱 | 說明 | 解決方式 |
|---|---|---|
忘記加 mut |
嘗試修改變數卻忘記在宣告時加 mut,會得到「cannot assign to immutable variable」的編譯錯誤。 |
在需要改變值的地方,先檢查變數是否真的需要是可變的,若是則加 mut。 |
| 誤用遮蔽 | 使用遮蔽時不小心把舊變數隱藏,導致後續程式碼仍在使用舊值,產生邏輯錯誤。 | 在遮蔽前加上註解說明意圖,或使用不同的變數名稱以提升可讀性。 |
| 可變參考與可變變數混淆 | 同時持有可變參考與可變變數會觸發借用檢查錯誤。 | 了解所有權與借用規則,盡量在同一作用域內只保留一個可變借用。 |
在 const 中使用 mut |
const 只能是不可變的,若需要全域可變狀態應使用 static mut(需 unsafe)。 |
大多數情況下避免全域可變,改用 Mutex、RwLock 包裝的 static。 |
過度使用 mut |
把所有變數都標記為 mut,失去 Rust 的安全保證。 |
僅在確實需要改變值的情況下使用 mut,其餘保持不可變。 |
最佳實踐
- 預設不可變:先寫
let,只有在真的需要改變時才改為let mut。 - 使用遮蔽做型別轉換:不必額外產生新變數名稱,保持程式碼簡潔。
- 盡量在最小作用域內宣告變數:減少變數生命週期,降低借用衝突的機會。
- 加入註解說明
mut的必要性:讓團隊成員快速了解為何此變數可變。
實際應用場景
計算器或統計分析
需要在迴圈中累加或更新統計值時,let mut total = 0;是最直觀的寫法。狀態機(State Machine)
透過可變變數保存當前狀態,並在每次事件觸發時更新,例如let mut state = State::Idle;。字串拼接與日誌
String必須是可變的才能使用push_str、push等方法,常見於建立動態訊息或 JSON 序列化。配置參數的預處理
先以let讀取環境變數,然後利用遮蔽把字串轉成適當的型別(usize、bool),最後再以不可變變數傳遞給其他模組。嵌入式系統的暫存緩衝
在資源受限的環境下,使用let mut buffer = [0u8; 128];來作為暫存區,隨時寫入感測器資料。
總結
let為 Rust 中宣告變數的關鍵字,預設不可變,提升程式安全與編譯器最佳化空間。mut必須明確寫在變數宣告前,才能允許在之後的程式碼中改變其值。- 遮蔽(shadowing) 提供了在同一作用域內重新宣告同名變數的能力,常用於型別轉換或暫時改變可變性。
- 正確使用
const、static、mut,配合所有權與借用規則,可寫出既安全又高效的 Rust 程式。
掌握變數宣告的細節,是踏入 Rust 世界的第一步。未來在處理更複雜的資料結構、併發程式或嵌入式開發時,這些基礎概念將持續為你提供堅實的基礎。祝你寫程式愉快,玩得開心!