Skip to main content

Rust 基本語法

Rust 是靜態型別、編譯型的系統語言。程式在編譯期就決定型別,並用**所有權(ownership)**在不靠垃圾回收的前提下管理記憶體。日常開發以 cargo 建專案、編譯與執行。工具鏈安裝見 安裝。

本文依 Rust 2024 edition(cargo new 自 Rust 1.85 起的預設)整理常用寫法。範例都可放進 fn main() 直接跑。


第一個程式​

cargo new hello
cd hello
cargo run

src/main.rs:

fn main() {
println!("Hello, world!");
}

Cargo.toml 會標 edition。新專案大致如下:

[package]
name = "hello"
version = "0.1.0"
edition = "2024"
指令用途
cargo run編譯並執行
cargo build編譯 debug 版,產物在 target/debug/
cargo build --release最佳化後的 release 版
cargo check只做型別檢查,不產生執行檔
cargo test跑 #[test] 測試
cargo fmt用 rustfmt 排版
cargo clippy靜態檢查,抓常見寫法問題

println! 結尾的 ! 表示這是巨集(macro),不是一般函式。格式化用 {},除錯用 {:?}(型別需實作 Debug):

let name = "Rust";
let version = 1.85;
println!("{name} {version}");
println!("name = {name:?}");

變數、常數與遮蔽​

預設不可變。要改值必須標 mut:

let x = 5;
let mut y = 5;
y += 1;

同一個名稱可以再 let 一次,這叫遮蔽(shadowing)。型別可以跟著變,適合「先拿到值、再轉成另一種型別」:

let spaces = "   ";
let spaces = spaces.len(); // usize

常數用 const,必須標型別,而且只能是編譯期就能算出的值:

const MAX_POINTS: u32 = 100_000;

數字可用底線分位,100_000 與 100000 相同。

static 是整個程式存活的值,可變的 static mut 要放在 unsafe 裡,一般程式少用。


基本型別​

純量​

型別說明
i8 i16 i32 i64 i128 isize有號整數。未標型別的整數字面量預設 i32
u8 … usize無號整數。切片長度、索引是 usize
f32 f64浮點數。未標型別預設 f64
booltrue / false
charUnicode 純量值,用單引號,例如 '中',佔 4 bytes

整數溢位:debug 建置會 panic,release 預設繞回(wrapping)。需要明確語意時用 checked_add、saturating_add、wrapping_add。

複合型別​

Tuple 可混不同型別,用 .0、.1 或解構取值:

let point: (i32, f64, char) = (1, 2.5, 'a');
let (x, y, c) = point;
println!("{} {} {}", x, point.1, c);

Array 長度固定,且長度是型別的一部分。[i32; 3] 與 [i32; 4] 是不同型別:

let nums: [i32; 3] = [1, 2, 3];
let zeros = [0; 5]; // [0, 0, 0, 0, 0]
println!("{}", nums[0]);

越界索引在執行期 panic。長度會變的序列用 Vec<T>。

Slice &[T] 是一段連續元素的借用,不擁有資料:

let nums = [1, 2, 3, 4];
let mid: &[i32] = &nums[1..3]; // [2, 3]

範圍:.. 不含結尾,..= 含結尾。0..3 是 0、1、2;0..=3 是 0、1、2、3。


函式與表達式​

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

fn greet(name: &str) {
println!("hi, {name}");
}
  • 參數與回傳型別都要寫出來。
  • 函式本體最後一個沒有分號的表達式就是回傳值。寫 return a + b; 也可以。
  • 沒有回傳值的函式,回傳型別是 unit (),通常省略不寫。
  • 區塊 { ... } 本身也是表達式,所以 if、match、loop 都能產生值。
let label = if score >= 60 { "及格" } else { "不及格" };

if 與 else 兩邊的型別必須相同。


控制流程​

if / else if​

條件必須是 bool。Rust 不會把數字自動轉成布林,if n { } 無法編譯。

loop、while、for​

let mut n = 0;
let result = loop {
n += 1;
if n == 3 {
break n * 2; // loop 可以用 break 帶值出來
}
};

while n > 0 {
n -= 1;
}

for i in 0..3 {
println!("{i}");
}

let names = ["ann", "bo"];
for name in names {
println!("{name}");
}

continue 進入下一輪,break 離開迴圈。巢狀迴圈可用生命週期標籤跳出外層:

'outer: for i in 0..3 {
for j in 0..3 {
if i == 1 && j == 1 {
break 'outer;
}
}
}

match​

match 必須涵蓋所有可能。比對由上往下,第一個符合的分支生效:

fn describe(n: i32) -> &'static str {
match n {
0 => "零",
1..=9 => "個位數",
n if n < 0 => "負數",
_ => "其他",
}
}

_ 是其餘所有情況。分支上的 if 叫 match guard。

只關心其中一種情況時用 if let:

if let Some(value) = maybe {
println!("{value}");
}

Rust 1.88 起、且 edition 為 2024 時,可以用 && 把多個 let 串在同一個條件裡:

let first = Some(1);
let second = Some(2);
if let Some(a) = first && let Some(b) = second {
println!("{}", a + b);
}

所有權與借用​

這是 Rust 和多數語言差最多的地方。三條規則:

  1. 每個值有一個擁有者(owner)。
  2. 同一時間只能有一個擁有者。
  3. 擁有者離開作用域,值就被丟棄(drop)。

把非 Copy 的值指派或傳進函式,所有權會移動(move),原來的變數不能再使用:

let s1 = String::from("hello");
let s2 = s1;
// println!("{s1}"); // 編譯錯誤:s1 已經被 move
println!("{s2}");

整數、bool、char、以及只含這些欄位的 tuple / array 有 Copy,賦值是複製,兩邊都還能用。String、Vec<T> 沒有 Copy,需要第二份擁有權時呼叫 .clone()(會配置新記憶體)。

借用​

不想把所有權交出去,就傳參考:

寫法意義
&T共享借用,可同時存在多個,不能透過它改值
&mut T可變借用,同一時間只能有一個,且不能與 &T 並存
fn len(s: &String) -> usize {
s.len()
}

fn push_exclaim(s: &mut String) {
s.push('!');
}

let mut name = String::from("hi");
println!("{}", len(&name));
push_exclaim(&mut name);

借用不能活得比擁有者久。回傳參考時,編譯器會用**生命週期(lifetime)**確認這一點。最常見的寫法是輸入與輸出綁同一個 'a:

fn longer<'a>(a: &'a str, b: &'a str) -> &'a str {
if a.len() >= b.len() { a } else { b }
}

只在函式內部建立、不把參考傳出去時,生命週期多半可以省略,編譯器會自行推斷。

String 與 &str​

型別是什麼
String擁有資料的可增長 UTF-8 字串,在 heap
&str一段字串的借用,字面量 "hello" 的型別是 &'static str

函式參數優先接 &str,呼叫端傳 String 或字面量都可以(String 會自動 deref 成 &str):

fn shout(s: &str) -> String {
format!("{s}!")
}

String 不能用 s[0] 取「第幾個字」。UTF-8 一個字元可能佔多個 byte,改走迭代器:

for ch in "你好".chars() {
println!("{ch}");
}

結構、列舉與特徵​

struct​

struct User {
name: String,
age: u8,
}

impl User {
fn new(name: impl Into<String>, age: u8) -> Self {
Self {
name: name.into(),
age,
}
}

fn greet(&self) -> String {
format!("{} 今年 {} 歲", self.name, self.age)
}

fn birthday(&mut self) {
self.age = self.age.saturating_add(1);
}
}
  • &self:借用自己,對應方法呼叫 user.greet()。
  • &mut self:可變借用。
  • self:拿走所有權,呼叫後原本的值不能再用。
  • 沒有 self 的函式是關聯函式,用 User::new(...) 呼叫。

欄位少、沒有名字也夠清楚時,可用 tuple struct:struct Point(i32, i32);。

用 #[derive] 讓編譯器自動實作常見特徵:

#[derive(Debug, Clone, PartialEq, Eq)]
struct Point {
x: i32,
y: i32,
}

有 Debug 才能 println!("{point:?}") 或 {:#?}(多行)。

enum​

列舉的每個分支可以帶不同資料:

enum Status {
Active,
Away { minutes: u32 },
Offline,
}

fn label(status: &Status) -> String {
match status {
Status::Active => "在線".to_string(),
Status::Away { minutes } => format!("離開 {minutes} 分鐘"),
Status::Offline => "離線".to_string(),
}
}

trait​

trait 定義一組方法,型別用 impl 提供實作。參數寫 impl Summary 表示「任何有實作 Summary 的型別」:

trait Summary {
fn summarize(&self) -> String;
}

fn notify(item: &impl Summary) {
println!("{}", item.summarize());
}

多個條件可寫成泛型:

fn notify<T: Summary + std::fmt::Debug>(item: &T) {
println!("{item:?}: {}", item.summarize());
}

很長的條件用 where:

fn dump<T>(item: &T)
where
T: Summary + std::fmt::Debug,
{
println!("{item:?}");
}

Option、Result 與 ?​

Rust 沒有 null。可能沒有值用 Option<T>,可能失敗用 Result<T, E>。

enum Option<T> {
Some(T),
None,
}

enum Result<T, E> {
Ok(T),
Err(E),
}

兩者都在 prelude 裡,直接用 Some、None、Ok、Err 即可。

fn first_even(nums: &[i32]) -> Option<i32> {
nums.iter().copied().find(|n| n % 2 == 0)
}

let n = first_even(&[1, 3, 4]).unwrap_or(0);
方法行為
unwrap()None / Err 時 panic。僅適合確定有值,或原型程式
expect("說明")同上,panic 訊息自己寫
unwrap_or(default)沒值時改用預設值
unwrap_or_else(|err| ...)沒值時呼叫閉包產生預設值
map / and_then有值才轉換,維持 Option / Result

? 表示:是 Err(或 None)就立刻從目前函式回傳,否則取出裡面的值。函式回傳型別必須相容。

fn sum_csv(input: &str) -> Result<i32, std::num::ParseIntError> {
let mut total = 0;
for part in input.split(',') {
let n: i32 = part.trim().parse()?;
total += n;
}
Ok(total)
}

main 也可以回傳 Result。錯誤時程序會以非 0 結束:

fn main() -> Result<(), Box<dyn std::error::Error>> {
let total = sum_csv("1, 2, 3")?;
println!("{total}");
Ok(())
}

字串轉數字是 .parse(),目標型別要標出來或讓上下文推斷:

let n: i32 = "42".parse()?;

集合與迭代器​

Vec<T>​

let mut nums = vec![1, 2, 3];
nums.push(4);
let third: Option<&i32> = nums.get(2); // 越界得到 None,不 panic

for n in &nums {
println!("{n}");
}

nums[i] 越界會 panic;不確定有沒有這個索引時用 .get(i)。

HashMap​

use std::collections::HashMap;

let mut scores = HashMap::new();
scores.insert(String::from("blue"), 10);
scores.entry(String::from("blue")).or_insert(0);

entry 適合「沒有這個 key 才寫入」。讀取得到 Option<&V>。

迭代器​

iter() 借出元素,iter_mut() 可變借用,into_iter() 拿走所有權。鏈上的 map、filter 是惰性的,遇到 collect、for 或 sum 才真的執行:

let doubled: Vec<i32> = [1, 2, 3]
.into_iter()
.map(|n| n * 2)
.filter(|n| *n > 2)
.collect();

閉包參數寫在 |...|。編譯器會推斷它是 Fn、FnMut 還是 FnOnce。


模組與可見性​

預設私有。對外要加 pub。

// src/lib.rs
mod user;

pub use user::User;
// src/user.rs
pub struct User {
pub name: String,
age: u8, // 模組外讀不到
}
路徑意義
crate::...從目前 crate 根目錄起算
super::...父模組
self::...目前模組

use std::collections::HashMap; 把名稱引進作用域。只在這個函式用到時,寫完整路徑也行。

程式進入點:

  • 執行檔:src/main.rs 的 fn main
  • 函式庫:src/lib.rs,給別人 use 你的 pub 項目

常見寫法速查​

// 有值才做,沒值就離開目前函式
let name = name_opt?;

// 有值才綁變數
if let Some(user) = find_user(id) {
println!("{}", user.name);
}

// 斷言,失敗即 panic。多放在測試或「理論上不可能」的分支
assert_eq!(add(2, 3), 5);

// 轉成擁有的 String
let owned: String = "hi".to_string();
let owned = "hi".to_owned();

// 從可能失敗的轉換取出值
let n = u32::try_from(len).unwrap_or(0);

測試寫在同一個檔案底部即可:

#[cfg(test)]
mod tests {
use super::*;

#[test]
fn two_plus_two() {
assert_eq!(2 + 2, 4);
}
}

執行:cargo test。