Rust 開發模式
用於建構安全、高效且易於維護應用程式的慣用 Rust 模式與最佳實踐。
何時使用
- 撰寫新的 Rust 程式碼
- 審查 Rust 程式碼
- 重構現有的 Rust 程式碼
- 設計 crate 結構與模組佈局
運作方式
此技能在六個關鍵領域強制執行慣用的 Rust 慣例:透過所有權與借用系統在編譯期防止資料競爭;使用 thiserror(函式庫)與 anyhow(應用程式)進行 Result/? 錯誤傳播;利用枚舉與窮舉模式匹配使非法狀態無法表示;透過 trait 與泛型實現零成本抽象;使用 Arc<Mutex<T>>、通道(channel)與 async/await 達成安全並發;以及按領域組織的最小公開(pub)介面。
核心原則
1. 所有權與借用
Rust 的所有權系統在編譯期防止資料競爭與記憶體錯誤。
// 良好:不需要所有權時傳遞引用
fn process(data: &[u8]) -> usize {
data.len()
}
// 良好:僅在需要儲存或消費時取得所有權
fn store(data: Vec<u8>) -> Record {
Record { payload: data }
}
// 不良:為了繞過借用檢查器而進行不必要的 clone
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(); // 恐慌!
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!("worker 數量必須大於 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 和未來的變體
}
Traits 與泛型
接受泛型,傳回具體型別
// 良好:泛型輸入,具體輸出
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)
}
// 良好:多重約束的 trait 邊界
fn process<T: Display + Send + 'static>(item: T) -> String {
format!("處理完畢: {item}")
}
用於動態分派的 Trait 物件
// 當需要異質集合或外掛系統時使用
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> {
// 不可能意外交換 user 和 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("工作執行緒恐慌");
}
使用通道進行訊息傳遞
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!("產生的任務恐慌: {e}")));
}
results
}
不安全程式碼
何時可以接受 Unsafe
// 可接受:帶有已記錄不變量的 FFI 邊界 (Rust 2024+)
/// # 安全性
/// `ptr` 必須是一個有效、對齊的指標,指向一個已初始化的 `Widget`。
unsafe fn widget_from_raw<'a>(ptr: *const Widget) -> &'a Widget {
// SAFETY: 呼叫者保證 ptr 有效且對齊
unsafe { &*ptr }
}
// 可接受:具有正確性證明的效能關鍵路徑
// SAFETY: 由於迴圈邊界限制,index 始終小於 len
unsafe { slice.get_unchecked(index) }
何時不可接受 Unsafe
// 不良:使用 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() | 傳播錯誤,切勿在函式庫/生產程式碼中恐慌 |
| 解析,不要驗證 | 在邊界處將非結構化資料轉換為型別化結構體 |
| 新類型確保型別安全 | 將基本型別包裝在新類型中以防止參數交換 |
| 優先使用迭代器而非迴圈 | 宣告式鏈更清晰,且通常更快 |
在 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,並讓型別系統為你工作。