English | 繁體中文

Rust — 單頁筆記

Rust 是一種編譯型、靜態型別的系統程式語言,專注於在沒有垃圾回收的情況下確保記憶體安全。所有權、借用、生命週期、代數資料型別、trait 與窮舉式模式比對,讓無效狀態難以被表示。優先使用 cargo fmt、cargo clippy、測試與小型且明確的抽象;讓編譯器說明下一個安全的變更。

工具鏈與 Cargo

rustup toolchain install stable       # 安裝/更新工具鏈
rustup default stable
cargo new hello                       # 二進位 crate
cargo new --lib library               # 函式庫 crate
cargo add serde --features derive     # 新增相依套件(cargo-edit)
cargo check                           # 快速進行型別檢查
cargo build --release                 # 在 target/release 中進行最佳化建置
cargo run --release -- arg1           # 建置並執行
cargo test                            # 單元與整合測試
cargo test -- --nocapture             # 顯示測試輸出
cargo fmt -- --check                  # 驗證標準格式
cargo clippy --all-targets --all-features -- -D warnings
cargo doc --open                      # 建置 API 文件
cargo tree                            # 檢視相依圖
rustc --edition 2021 main.rs         # 編譯獨立檔案

套件包含 Cargo.toml 與一個或多個 crate。src/main.rs 是二進位程式;src/lib.rs 是函式庫。相關套件可使用 workspace。保持公開 API 精簡、為公開項目撰寫文件、固定或審查相依套件更新;應用程式應提交 Cargo.lock(函式庫可以省略)。

語法、繫結與核心型別

fn main() {
    let answer = 42;                 // 預設不可變;推斷為 i32
    let mut count: u64 = 0;          // 必須明確指定可變性
    count += 1;
    const MAX_RETRIES: usize = 3;    // 編譯期常數
    let shadow = "text";
    let shadow = shadow.len();       // 遮蔽可以改變型別
    println!("{answer} {count} {MAX_RETRIES} {shadow}");
}
 
// 純量型別:bool、char(Unicode 純量值)、有號/無號整數、
// f32/f64。複合型別:tuple 與固定大小的陣列。
let pair: (i32, &str) = (7, "seven");
let (number, word) = pair;           // 解構模式
let bytes: [u8; 3] = [1, 2, 3];
let repeated = [0; 4];
let slice: &[u8] = &bytes[1..];      // 借用的檢視;不取得所有權
 
// 集合擁有堆積資料,通常透過 `let mut` 使其可變。
let mut text = String::from("Rust");
text.push_str("!\n");
let mut values = vec![1, 2, 3];
values.push(4);
let map = std::collections::HashMap::from([("rust", 2010)]);
 
// 轉換是明確的:原始型別轉型使用 `as`;字串使用 `parse`。
let n: u32 = "42".parse().expect("valid number");
let widened = n as u64;

運算式會回傳值;陳述式以 ; 結束。區塊中的最後一個運算式就是其結果。if、match 與迴圈都是運算式。不存在隱含的真值判定:條件必須是 bool。

let label = if count > 0 { "non-empty" } else { "empty" };
for value in [10, 20, 30] { println!("{value}"); }
for i in 0..3 { println!("{i}"); }       // 0、1、2
for i in (0..=3).rev() { println!("{i}"); }
let mut n = 0;
while n < 3 { n += 1; }
let result = loop {
    n += 1;
    if n == 5 { break n * 2; }
};
 
'outer: for row in 0..3 {
    for col in 0..3 {
        if row == col { break 'outer; }
    }
}

所有權、移動、複製與借用

每個值都有一個所有者。同一時間恰好只有一個所有者;所有者離開作用域時,其值就會被丟棄。指派、引數傳遞與回傳通常會移動所有權。實作 Copy 的型別(數字、bool、char、共用不可變參照,以及由 Copy 值組成的小型 tuple)則會改為複製。

fn take(text: String) -> usize { text.len() } // 消耗 String
fn borrow(text: &str) -> usize { text.len() } // 共用借用;不會消耗
fn append(text: &mut String) { text.push('!') } // 獨佔借用
 
let original = String::from("hello");
let moved = original;                 // String 已移動;`original` 不再可用
let length = borrow(&moved);          // 共用借用
let mut owned = moved;
append(&mut owned);                   // 獨佔借用
println!("{owned} ({length})");
 
let a = 10;
let b = a;                            // i32 實作 Copy;兩者都仍可使用

參照不能比其所參照的值活得更久。Rust 在編譯期阻止懸空參照與資料競爭。在同一時間,一個值可以有許多不可變(&T)借用,或一個可變(&mut T)借用,但不能兩者兼有;借用也必須在使用期間保持有效。

fn first_word(text: &str) -> &str {
    text.split_whitespace().next().unwrap_or("")
}
 
let mut name = String::from("Ada Lovelace");
let first = first_word(&name);         // 不可變借用在最後一次使用後結束
println!("{first}");
name.push_str(" (1815)");              // 現在允許可變借用

借用文字使用 &str,擁有且可增長的文字使用 String。&[T] 是借用的 slice;Vec<T> 擁有可增長的緩衝區。Box<T> 擁有一個堆積配置,Rc<T> 提供單執行緒共用所有權,而 Arc<T> 提供具執行緒安全性的參照計數。RefCell<T>/Mutex<T> 將部分借用檢查移至執行期;違反規則時可能造成 panic 或 poisoning,而不是成為編譯錯誤。

Struct、enum、方法與模式

#[derive(Debug, Clone, PartialEq, Eq)]
struct User {
    id: u64,
    name: String,
}
 
enum Message {
    Quit,
    Move { x: i32, y: i32 },
    Text(String),
    ChangeColor(u8, u8, u8),
}
 
impl User {
    fn label(&self) -> String { format!("{}: {}", self.id, self.name) }
    fn rename(&mut self, name: impl Into<String>) { self.name = name.into(); }
}
 
fn describe(message: Message) -> String {
    match message {
        Message::Quit => "quit".into(),
        Message::Move { x, y } => format!("move to ({x}, {y})"),
        Message::Text(text) if text.is_empty() => "empty text".into(),
        Message::Text(text) => text,
        Message::ChangeColor(r, g, b) => format!("rgb({r}, {g}, {b})"),
    }
}
 
let mut user = User { id: 1, name: "Ada".into() };
user.rename("Grace");
let User { id, ref name } = user;       // `ref` 在模式中借用欄位
println!("{id}: {name}");
 
if let Some(value) = Some(3) { println!("{value}"); }
let status = match 404 {
    200..=299 => "success",             // 包含端點的範圍模式
    400 | 404 => "client error",        // 或模式
    _ => "other",                       // 萬用字元;match 必須完整涵蓋
};

模式會出現在 let、match、if let、while let、for 與函式參數中。適當使用 ref、@、..、範圍、守衛(if condition)與 matches!。Option<T> 表示存在(Some)或不存在(None);永遠不要使用 null。Result<T, E> 表示成功或失敗。

函式、泛型、trait 與生命週期

fn sum<T>(items: impl IntoIterator<Item = T>) -> T
where
    T: std::ops::Add<Output = T> + Default,
{
    items.into_iter().fold(T::default(), |total, item| total + item)
}
 
trait Summarize {
    fn summary(&self) -> String;
    fn short(&self) -> String { self.summary().chars().take(20).collect() }
}
 
impl Summarize for User {
    fn summary(&self) -> String { self.label() }
}
 
fn longest<'a>(left: &'a str, right: &'a str) -> &'a str {
    if left.len() >= right.len() { left } else { right }
}

泛型預設會被單態化,提供靜態派送且沒有固有的執行期成本。Trait bound 限制可用的能力。impl Trait 適合用於參數與不透明回傳型別;dyn Trait 是以堆積為後盾、用於執行期派送的 trait 物件,例如 Box<dyn Error + Send + Sync>。生命週期描述參照之間的關係;它們不會延長底層值的生命週期。大多數生命週期會被推斷。只有在值擁有其資料,或確實存活整個程式期間時,才使用 'static。

閉包可以透過共用借用、可變借用或移動來捕獲。move 強制取得捕獲值的所有權,在產生執行緒時很常見。Iterator 配接器(map、filter、zip、take)是惰性的;collect、sum、for_each 與 fold 等終端方法會執行它們。

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

錯誤、? 與資源安全

可復原的錯誤回傳 Result<T, E>,預期的缺失回傳 Option<T>。? 會在失敗時提早回傳,並透過 From 轉換相容的錯誤。適合時,函式庫錯誤使用 thiserror 表示,應用程式層級的情境則使用 anyhow。Drop 會在作用域離開時自動執行;使用 RAII guard 進行清理與鎖定釋放。panic! 用於違反不變量或無法復原的錯誤,不要用於例行的輸入驗證。

use std::{fs, io, path::Path};
 
fn read_config(path: &Path) -> Result<String, io::Error> {
    let text = fs::read_to_string(path)?;
    if text.trim().is_empty() {
        return Err(io::Error::new(io::ErrorKind::InvalidData, "empty config"));
    }
    Ok(text)
}

避免在不受信任的輸入或正式環境路徑上使用 unwrap/expect;當不變量在區域內明顯成立且失敗應中止時,它們才適合使用。絕不要意外丟棄 Result。在邊界驗證路徑與外部資料;避免透過字串串接來建立 shell 指令或 SQL。

並行與非同步

Rust 的 Send 與 Sync trait 讓執行緒安全成為型別系統的一部分。std::thread::spawn 要求捕獲的值符合 'static;move 將所有權轉移到執行緒中。Join handle 會以 Result 傳遞 panic。使用 channel 轉移所有權,並使用 Arc<Mutex<T>>/Arc<RwLock<T>> 管理共用的可變狀態。優先採用訊息傳遞或不可變資料,而不是廣泛加鎖。Atomic 適合具有明確排序模型的小型無鎖狀態。

use std::sync::{mpsc, Arc, Mutex};
use std::thread;
 
fn main() {
    let (jobs_tx, jobs_rx) = mpsc::channel::<u32>();
    let jobs_rx = Arc::new(Mutex::new(jobs_rx));
    let mut workers = Vec::new();
 
    for _ in 0..2 {
        let jobs_rx = Arc::clone(&jobs_rx);
        workers.push(thread::spawn(move || {
            let mut total = 0;
            loop {
                let job = jobs_rx.lock().expect("receiver lock").recv();
                match job {
                    Ok(value) => total += value * value,
                    Err(_) => return total,
                }
            }
        }));
    }
    for value in 1..=4 { jobs_tx.send(value).expect("workers are alive"); }
    drop(jobs_tx);                       // 關閉工作串流
    let total: u32 = workers.into_iter()
        .map(|worker| worker.join().expect("worker did not panic"))
        .sum();
    println!("total={total}");
}

標準函式庫提供執行緒、channel、Mutex、RwLock、Condvar、OnceLock 與 atomic。rayon 適合 CPU 密集的資料平行處理。async fn 會回傳 Future;在 Tokio 或 async-std 等執行器等待之前,它不會執行。非同步工作是協作式的:避免在非同步執行環境的執行緒上進行阻塞呼叫;使用執行環境感知的 I/O 與有界並行。Send 允許在執行緒之間移動 future;Sync 允許共用參照。取消通常以丟棄 future,或使用明確的 token/channel 來表示。

模組、巨集、unsafe 與 FFI

mod geometry {
    pub struct Point { pub x: f64, pub y: f64 }
    pub fn distance(a: Point, b: Point) -> f64 {
        ((a.x - b.x).powi(2) + (a.y - b.y).powi(2)).sqrt()
    }
}
 
use geometry::Point;

模組預設為私有;pub 公開項目,pub(crate) 則在 crate 內公開。crate::、self:: 與 super:: 適合用於絕對/相對路徑。宣告式巨集使用 macro_rules!;derive/程序巨集會產生程式碼(#[derive(Debug)]、#[test])。

unsafe 不是效能開關:它允許解參照原始指標、呼叫不安全函式、存取可變靜態變數、存取 union 欄位,以及實作不安全 trait。讓 unsafe 區塊保持微小,記錄使其健全的不變量,以安全 API 包裝它們,並在可行時執行 Miri/sanitizer。FFI 邊界需要 #[repr(C)]、正確的所有權/記憶體配置,以及明確處理 null 指標與錯誤碼。

標準函式庫導覽

  • 集合:Vec、VecDeque、LinkedList(少見)、HashMap、HashSet、BTreeMap、BTreeSet、BinaryHeap。
  • 文字/資料:String、str、char、std::fmt、std::str、std::convert;JSON/序列化則在外部使用 serde。
  • 檔案/程序:std::fs、std::path、std::io、std::env、std::process、std::ffi。
  • 時間/網路:std::time、std::net;正式環境的 HTTP client 使用 reqwest/Tokio 或同等工具。
  • 並行:std::thread、std::sync、std::sync::atomic;更豐富的有作用域 channel/執行緒可使用 crossbeam。
  • 迭代:Iterator、IntoIterator、iter、iter_mut、into_iter、FromIterator。
  • 檢視:Debug({:?})、Display({})、assert!、dbg!、std::backtrace。

測試、效能與設計檢查清單

#[cfg(test)]
mod tests {
    use super::*;
 
    #[test]
    fn longest_returns_the_longer_slice() {
        assert_eq!(longest("short", "longer"), "longer");
    }
}

在程式碼旁撰寫單元測試,在 tests/ 中撰寫整合測試,在文件中撰寫 doctest,並在有用時使用 proptest 或 cargo-fuzz 撰寫屬性/模糊測試。在 CI 中執行 cargo test、cargo clippy 與 cargo fmt -- --check。使用 criterion 進行基準測試,而不是憑猜測;最佳化前先分析效能。當迭代器能讓意圖更清楚時優先使用,但要測量配置與熱點路徑。使用 cargo audit/cargo deny 維護相依套件衛生,並使用 cargo +nightly miri test 檢查某些未定義行為。

**慣用法:**透過 enum/newtype 讓無效狀態不可表示;回傳 Result 而不是哨兵值;函式不需要取得所有權時就借用;在能改善 API 時接受 impl AsRef<str>/slice;使用 &[T] 而不是 &Vec<T>;有意識地衍生標準 trait;隔離 unsafe;避免不必要的 clone;只有在所有權設計需要共用可變性時才使用 Arc/Mutex;記錄不變量與公開的安全契約;優先採用窮舉式 match;讓錯誤情境可採取行動。