rust-patterns

rust-patterns

熱門

慣用的 Rust 模式、所有權、錯誤處理、特徵、並行處理以及建構安全高效能應用程式的最佳實務。

23萬星標
3.5萬分支
更新於 2026/7/17
SKILL.md
readonlyread-only
name
rust-patterns
description

慣用的 Rust 模式、所有權、錯誤處理、特徵、並行處理以及建構安全高效能應用程式的最佳實務。

Rust 開發模式

慣用的 Rust 模式與最佳實務,用於建構安全、高效能且易於維護的應用程式。

使用時機

  • 撰寫新的 Rust 程式碼
  • 審查 Rust 程式碼
  • 重構現有的 Rust 程式碼
  • 設計 crate 結構與模組佈局

運作方式

此技能在六大關鍵領域強制執行慣用的 Rust 慣例:所有權與借用(在編譯時防止資料競爭)、使用 thiserror 進行函式庫的 Result/? 錯誤傳播、使用 anyhow 進行應用程式的錯誤處理、列舉與窮舉模式匹配(使非法狀態無法表示)、特徵與泛型(零成本抽象)、透過 Arc<Mutex<T>>、通道與 async/await 的安全並行處理,以及按領域組織的最小 pub 表面。

核心原則

1. 所有權與借用

Rust 的所有權系統在編譯時防止資料競爭與記憶體錯誤。

// 好:不需要所有權時傳遞參考
fn process(data: &[u8]) -> usize {
    data.len()
}

// 好:僅在需要儲存或消耗時取得所有權
fn store(data: Vec<u8>) -> Record {
    Record { payload: data }
}

// 壞:為了繞過借用檢查器而進行不必要的複製
fn process_bad(data: &Vec<u8>) -> usize {
    let cloned = data.clone(); // 浪費——只需借用
    cloned.len()
}

使用 Cow 實現靈活的所有權

use std::borrow::Cow;

fn normalize(input: &str) -> Cow<'_, str> {
    if input.contains(' ') {
        Cow::Owned(input.replace(' ', "_"))
    } else {
        Cow::Borrowed(input) // 不需要修改時零成本
    }
}

錯誤處理

使用 Result? —— 生產環境中絕不使用 unwrap()

// 好:傳播錯誤並附帶上下文
use anyhow::{Context, Result};

fn load_config(path: &str) -> Result<Config> {
    let content = std::fs::read_to_string(path)
        .with_context(|| format!("無法從 {path} 讀取設定檔"))?;
    let config: Config = toml::from_str(&content)
        .with_context(|| format!("無法從 {path} 解析設定檔"))?;
    Ok(config)
}

// 壞:錯誤時直接 panic
fn load_config_bad(path: &str) -> Config {
    let content = std::fs::read_to_string(path).unwrap(); // 會 panic!
    toml::from_str(&content).unwrap()
}

函式庫錯誤用 thiserror,應用程式錯誤用 anyhow

// 函式庫程式碼:結構化、型別化的錯誤
use thiserror::Error;

#[derive(Debug, Error)]
pub enum StorageError {
    #[error("找不到記錄:{id}")]
    NotFound { id: String },
    #[error("連線失敗")]
    Connection(#[from] std::io::Error),
    #[error("無效資料:{0}")]
    InvalidData(String),
}

// 應用程式程式碼:靈活的錯誤處理
use anyhow::{bail, Result};

fn run() -> Result<()> {
    let config = load_config("app.toml")?;
    if config.workers == 0 {
        bail!("工作者數量必須大於 0");
    }
    Ok(())
}

使用 Option 組合子取代巢狀匹配

// 好:組合子鏈
fn find_user_email(users: &[User], id: u64) -> Option<String> {
    users.iter()
        .find(|u| u.id == id)
        .map(|u| u.email.clone())
}

// 壞:深度巢狀匹配
fn find_user_email_bad(users: &[User], id: u64) -> Option<String> {
    match users.iter().find(|u| u.id == id) {
        Some(user) => match &user.email {
            email => Some(email.clone()),
        },
        None => None,
    }
}

列舉與模式匹配

將狀態建模為列舉

// 好:不可能狀態無法表示
enum ConnectionState {
    Disconnected,
    Connecting { attempt: u32 },
    Connected { session_id: String },
    Failed { reason: String, retries: u32 },
}

fn handle(state: &ConnectionState) {
    match state {
        ConnectionState::Disconnected => connect(),
        ConnectionState::Connecting { attempt } if *attempt > 3 => abort(),
        ConnectionState::Connecting { .. } => wait(),
        ConnectionState::Connected { session_id } => use_session(session_id),
        ConnectionState::Failed { retries, .. } if *retries < 5 => retry(),
        ConnectionState::Failed { reason, .. } => log_failure(reason),
    }
}

窮舉匹配 —— 商業邏輯中不使用萬用字元

// 好:明確處理每個變體
match command {
    Command::Start => start_service(),
    Command::Stop => stop_service(),
    Command::Restart => restart_service(),
    // 新增變體時會強制在此處理
}

// 壞:萬用字元隱藏了新變體
match command {
    Command::Start => start_service(),
    _ => {} // 默默地忽略 Stop、Restart 以及未來的變體
}

特徵與泛型

接受泛型,回傳具體型別

// 好:泛型輸入,具體輸出
fn read_all(reader: &mut impl Read) -> std::io::Result<Vec<u8>> {
    let mut buf = Vec::new();
    reader.read_to_end(&mut buf)?;
    Ok(buf)
}

// 好:多個約束的特徵邊界
fn process<T: Display + Send + 'static>(item: T) -> String {
    format!("已處理:{item}")
}

用於動態分派的特徵物件

// 當需要異質集合或外掛系統時使用
trait Handler: Send + Sync {
    fn handle(&self, request: &Request) -> Response;
}

struct Router {
    handlers: Vec<Box<dyn Handler>>,
}

// 需要效能時使用泛型(單態化)
fn fast_process<H: Handler>(handler: &H, request: &Request) -> Response {
    handler.handle(request)
}

用於型別安全的新類型模式

// 好:不同的型別防止參數混淆
struct UserId(u64);
struct OrderId(u64);

fn get_order(user: UserId, order: OrderId) -> Result<Order> {
    // 不會意外交換使用者與訂單 ID
    todo!()
}

// 壞:容易交換參數
fn get_order_bad(user_id: u64, order_id: u64) -> Result<Order> {
    todo!()
}

結構體與資料建模

用於複雜建構的 Builder 模式

struct ServerConfig {
    host: String,
    port: u16,
    max_connections: usize,
}

impl ServerConfig {
    fn builder(host: impl Into<String>, port: u16) -> ServerConfigBuilder {
        ServerConfigBuilder { host: host.into(), port, max_connections: 100 }
    }
}

struct ServerConfigBuilder { host: String, port: u16, max_connections: usize }

impl ServerConfigBuilder {
    fn max_connections(mut self, n: usize) -> Self { self.max_connections = n; self }
    fn build(self) -> ServerConfig {
        ServerConfig { host: self.host, port: self.port, max_connections: self.max_connections }
    }
}

// 使用方式:ServerConfig::builder("localhost", 8080).max_connections(200).build()

迭代器與閉包

偏好迭代器鏈而非手動迴圈

// 好:宣告式、惰性、可組合
let active_emails: Vec<String> = users.iter()
    .filter(|u| u.is_active)
    .map(|u| u.email.clone())
    .collect();

// 壞:指令式累積
let mut active_emails = Vec::new();
for user in &users {
    if user.is_active {
        active_emails.push(user.email.clone());
    }
}

使用帶型別註解的 collect()

// 收集到不同型別
let names: Vec<_> = items.iter().map(|i| &i.name).collect();
let lookup: HashMap<_, _> = items.iter().map(|i| (i.id, i)).collect();
let combined: String = parts.iter().copied().collect();

// 收集 Results —— 在第一個錯誤時短路
let parsed: Result<Vec<i32>, _> = strings.iter().map(|s| s.parse()).collect();

並行處理

用於共享可變狀態的 Arc<Mutex<T>>

use std::sync::{Arc, Mutex};

let counter = Arc::new(Mutex::new(0));
let handles: Vec<_> = (0..10).map(|_| {
    let counter = Arc::clone(&counter);
    std::thread::spawn(move || {
        let mut num = counter.lock().expect("mutex 已中毒");
        *num += 1;
    })
}).collect();

for handle in handles {
    handle.join().expect("工作者執行緒 panic");
}

用於訊息傳遞的通道

use std::sync::mpsc;

let (tx, rx) = mpsc::sync_channel(16); // 有界通道,具備背壓

for i in 0..5 {
    let tx = tx.clone();
    std::thread::spawn(move || {
        tx.send(format!("訊息 {i}")).expect("接收端已斷線");
    });
}
drop(tx); // 關閉發送端,讓 rx 迭代器終止

for msg in rx {
    println!("{msg}");
}

使用 Tokio 的非同步

use tokio::time::Duration;

async fn fetch_with_timeout(url: &str) -> Result<String> {
    let response = tokio::time::timeout(
        Duration::from_secs(5),
        reqwest::get(url),
    )
    .await
    .context("請求超時")?
    .context("請求失敗")?;

    response.text().await.context("無法讀取回應內容")
}

// 產生並行任務
async fn fetch_all(urls: Vec<String>) -> Vec<Result<String>> {
    let handles: Vec<_> = urls.into_iter()
        .map(|url| tokio::spawn(async move {
            fetch_with_timeout(&url).await
        }))
        .collect();

    let mut results = Vec::with_capacity(handles.len());
    for handle in handles {
        results.push(handle.await.unwrap_or_else(|e| panic!("產生的任務 panic:{e}")));
    }
    results
}

不安全的程式碼

何時可以接受不安全程式碼

// 可接受:具有文件化不變量的 FFI 邊界(Rust 2024+)
/// # 安全性
/// `ptr` 必須是已初始化 `Widget` 的有效且對齊的指標。
unsafe fn widget_from_raw<'a>(ptr: *const Widget) -> &'a Widget {
    // 安全性:呼叫者保證 ptr 有效且對齊
    unsafe { &*ptr }
}

// 可接受:具有正確性證明的效能關鍵路徑
// 安全性:由於迴圈邊界,index 始終小於 len
unsafe { slice.get_unchecked(index) }

何時不可接受不安全程式碼

// 壞:使用 unsafe 繞過借用檢查器
// 壞:為了方便而使用 unsafe
// 壞:使用 unsafe 但沒有安全性註解
// 壞:在不相關的型別之間進行 transmute

模組系統與 Crate 結構

按領域組織,而非按型別

my_app/
├── src/
│   ├── main.rs
│   ├── lib.rs
│   ├── auth/          # 領域模組
│   │   ├── mod.rs
│   │   ├── token.rs
│   │   └── middleware.rs
│   ├── orders/        # 領域模組
│   │   ├── mod.rs
│   │   ├── model.rs
│   │   └── service.rs
│   └── db/            # 基礎設施
│       ├── mod.rs
│       └── pool.rs
├── tests/             # 整合測試
├── benches/           # 基準測試
└── Cargo.toml

可見性 —— 最小化公開

// 好:pub(crate) 用於內部共享
pub(crate) fn validate_input(input: &str) -> bool {
    !input.is_empty()
}

// 好:從 lib.rs 重新匯出公開 API
pub mod auth;
pub use auth::AuthMiddleware;

// 壞:將所有東西都設為 pub
pub fn internal_helper() {} // 應該是 pub(crate) 或私有

工具整合

基本指令

# 建置與檢查
cargo build
cargo check              # 快速型別檢查,不產生程式碼
cargo clippy             # 提示與建議
cargo fmt                # 格式化程式碼

# 測試
cargo test
cargo test -- --nocapture    # 顯示 println 輸出
cargo test --lib             # 僅單元測試
cargo test --test integration # 僅整合測試

# 依賴
cargo audit              # 安全性稽核
cargo tree               # 依賴樹
cargo update             # 更新依賴

# 效能
cargo bench              # 執行基準測試

快速參考:Rust 慣用寫法

慣用寫法 說明
借用,不要複製 除非需要所有權,否則傳遞 &T 而非複製
使非法狀態無法表示 使用列舉僅建模有效狀態
使用 ? 而非 unwrap() 傳播錯誤,絕不在函式庫/生產程式碼中 panic
解析,不要驗證 在邊界處將非結構化資料轉換為型別化結構體
使用新類型確保型別安全 將基本型別包裝在新類型中以防止參數交換
偏好迭代器而非迴圈 宣告式鏈更清晰且通常更快
在 Result 上使用 #[must_use] 確保呼叫者處理回傳值
使用 Cow 實現靈活所有權 當借用足夠時避免分配
窮舉匹配 對商業關鍵列舉不使用萬用字元 _
最小化 pub 表面 對內部 API 使用 pub(crate)

應避免的反模式

// 壞:在生產程式碼中使用 .unwrap()
let value = map.get("key").unwrap();

// 壞:為了滿足借用檢查器而不理解原因就使用 .clone()
let data = expensive_data.clone();
process(&original, &data);

// 壞:在 &str 足夠時使用 String
fn greet(name: String) { /* 應該是 &str */ }

// 壞:在函式庫中使用 Box<dyn Error>(應使用 thiserror)
fn parse(input: &str) -> Result<Data, Box<dyn std::error::Error>> { todo!() }

// 壞:忽略 must_use 警告
let _ = validate(input); // 默默地丟棄 Result

// 壞:在非同步上下文中阻塞
async fn bad_async() {
    std::thread::sleep(Duration::from_secs(1)); // 阻塞執行器!
    // 應使用:tokio::time::sleep(Duration::from_secs(1)).await;
}

請記住:如果編譯通過,它很可能是正確的——但前提是你避免使用 unwrap()、最小化 unsafe,並讓型別系統為你工作。