Rust 開発パターン
安全で高性能かつ保守性の高いアプリケーションを構築するための慣用的なRustパターンとベストプラクティス。
使用場面
- 新しいRustコードを書く場合
- Rustコードをレビューする場合
- 既存のRustコードをリファクタリングする場合
- クレート構造とモジュールレイアウトを設計する場合
動作原理
このスキルは6つの重要な領域で慣用的なRustの規約を強制する:コンパイル時のデータ競合防止のための所有権と借用、ライブラリではthiserror、アプリケーションではanyhowを使用したResult/?エラー伝播、不正な状態を表現不可能にする列挙型と完全パターンマッチング、ゼロコスト抽象化のためのトレイトとジェネリクス、Arc<Mutex<T>>、チャンネル、async/awaitによる安全な並行処理、ドメインで整理された最小化されたpubインターフェース。
コア原則
1. 所有権と借用
Rustの所有権システムはコンパイル時にデータ競合とメモリエラーを防ぐ。
rust
// Good: Pass references when you don't need ownershipfn process(data: &[u8]) -> usize { data.len()}
// Good: Take ownership only when you need to store or consumefn store(data: Vec<u8>) -> Record { Record { payload: data }}
// Bad: Cloning unnecessarily to avoid borrow checkerfn process_bad(data: &Vec<u8>) -> usize { let cloned = data.clone(); // Wasteful — just borrow cloned.len()}柔軟な所有権のための Cow の使用
rust
use std::borrow::Cow;
fn normalize(input: &str) -> Cow<'_, str> { if input.contains(' ') { Cow::Owned(input.replace(' ', "_")) } else { Cow::Borrowed(input) // Zero-cost when no mutation needed }}エラー処理
Result と ? を使用する——本番環境では unwrap() を絶対に使わない
rust
// Good: Propagate errors with contextuse anyhow::{Context, Result};
fn load_config(path: &str) -> Result<Config> { let content = std::fs::read_to_string(path) .with_context(|| format!("failed to read config from {path}"))?; let config: Config = toml::from_str(&content) .with_context(|| format!("failed to parse config from {path}"))?; Ok(config)}
// Bad: Panics on errorfn load_config_bad(path: &str) -> Config { let content = std::fs::read_to_string(path).unwrap(); // Panics! toml::from_str(&content).unwrap()}ライブラリエラーには thiserror、アプリケーションエラーには anyhow
rust
// Library code: structured, typed errorsuse thiserror::Error;
#[derive(Debug, Error)]pub enum StorageError { #[error("record not found: {id}")] NotFound { id: String }, #[error("connection failed")] Connection(#[from] std::io::Error), #[error("invalid data: {0}")] InvalidData(String),}
// Application code: flexible error handlinguse anyhow::{bail, Result};
fn run() -> Result<()> { let config = load_config("app.toml")?; if config.workers == 0 { bail!("worker count must be > 0"); } Ok(())}ネストしたマッチの代わりに Option コンビネーターを優先する
rust
// Good: Combinator chainfn find_user_email(users: &[User], id: u64) -> Option<String> { users.iter() .find(|u| u.id == id) .map(|u| u.email.clone())}
// Bad: Deeply nested matchingfn 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, }}列挙型とパターンマッチング
状態を列挙型としてモデル化する
rust
// Good: Impossible states are unrepresentableenum 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), }}完全マッチング——ビジネスロジックではワイルドカードを使わない
rust
// Good: Handle every variant explicitlymatch command { Command::Start => start_service(), Command::Stop => stop_service(), Command::Restart => restart_service(), // Adding a new variant forces handling here}
// Bad: Wildcard hides new variantsmatch command { Command::Start => start_service(), _ => {} // Silently ignores Stop, Restart, and future variants}トレイトとジェネリクス
ジェネリックを受け取り、具体的な型を返す
rust
// Good: Generic input, concrete outputfn read_all(reader: &mut impl Read) -> std::io::Result<Vec<u8>> { let mut buf = Vec::new(); reader.read_to_end(&mut buf)?; Ok(buf)}
// Good: Trait bounds for multiple constraintsfn process<T: Display + Send + 'static>(item: T) -> String { format!("processed: {item}")}動的ディスパッチにトレイトオブジェクトを使用する
rust
// Use when you need heterogeneous collections or plugin systemstrait Handler: Send + Sync { fn handle(&self, request: &Request) -> Response;}
struct Router { handlers: Vec<Box<dyn Handler>>,}
// Use generics when you need performance (monomorphization)fn fast_process<H: Handler>(handler: &H, request: &Request) -> Response { handler.handle(request)}型安全のためにNewTypeパターンを使用する
rust
// Good: Distinct types prevent mixing up argumentsstruct UserId(u64);struct OrderId(u64);
fn get_order(user: UserId, order: OrderId) -> Result<Order> { // Can't accidentally swap user and order IDs todo!()}
// Bad: Easy to swap argumentsfn get_order_bad(user_id: u64, order_id: u64) -> Result<Order> { todo!()}構造体とデータモデリング
複雑な構築にはビルダーパターンを使用する
rust
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 } }}
// Usage: ServerConfig::builder("localhost", 8080).max_connections(200).build()イテレーターとクロージャー
手動ループの代わりにイテレーターチェーンを優先する
rust
// Good: Declarative, lazy, composablelet active_emails: Vec<String> = users.iter() .filter(|u| u.is_active) .map(|u| u.email.clone()) .collect();
// Bad: Imperative accumulationlet mut active_emails = Vec::new();for user in &users { if user.is_active { active_emails.push(user.email.clone()); }}型注釈付きの collect() を使用する
rust
// Collect into different typeslet 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();
// Collect Results — short-circuits on first errorlet parsed: Result<Vec<i32>, _> = strings.iter().map(|s| s.parse()).collect();並行処理
共有可変状態には Arc<Mutex<T>> を使用する
rust
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 poisoned"); *num += 1; })}).collect();
for handle in handles { handle.join().expect("worker thread panicked");}メッセージパッシングにチャンネルを使用する
rust
use std::sync::mpsc;
let (tx, rx) = mpsc::sync_channel(16); // Bounded channel with backpressure
for i in 0..5 { let tx = tx.clone(); std::thread::spawn(move || { tx.send(format!("message {i}")).expect("receiver disconnected"); });}drop(tx); // Close sender so rx iterator terminates
for msg in rx { println!("{msg}");}Tokioを使用した非同期プログラミング
rust
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("request timed out")? .context("request failed")?;
response.text().await.context("failed to read body")}
// Spawn concurrent tasksasync 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!("spawned task panicked: {e}"))); } results}アンセーフコード
Unsafe を使用できる場合
rust
// Acceptable: FFI boundary with documented invariants (Rust 2024+)/// # Safety/// `ptr` must be a valid, aligned pointer to an initialized `Widget`.unsafe fn widget_from_raw<'a>(ptr: *const Widget) -> &'a Widget { // SAFETY: caller guarantees ptr is valid and aligned unsafe { &*ptr }}
// Acceptable: Performance-critical path with proof of correctness// SAFETY: index is always < len due to the loop boundunsafe { slice.get_unchecked(index) }Unsafe を使用してはいけない場合
rust
// Bad: Using unsafe to bypass borrow checker// Bad: Using unsafe for convenience// Bad: Using unsafe without a Safety comment// Bad: Transmuting between unrelated typesモジュールシステムとクレート構造
型ではなくドメインで整理する
text
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可視性——露出を最小化する
rust
// Good: pub(crate) for internal sharingpub(crate) fn validate_input(input: &str) -> bool { !input.is_empty()}
// Good: Re-export public API from lib.rspub mod auth;pub use auth::AuthMiddleware;
// Bad: Making everything pubpub fn internal_helper() {} // Should be pub(crate) or privateツール統合
基本コマンド
bash
# Build and checkcargo buildcargo check # Fast type checking without codegencargo clippy # Lints and suggestionscargo fmt # Format code
# Testingcargo testcargo test -- --nocapture # Show println outputcargo test --lib # Unit tests onlycargo test --test integration # Integration tests only
# Dependenciescargo audit # Security auditcargo tree # Dependency treecargo update # Update dependencies
# Performancecargo bench # Run benchmarksクイックリファレンス:Rustのイディオム
| イディオム | 説明 |
|---|---|
| 借用、クローンではなく | 所有権が必要でなければ &T を渡し、クローンしない |
| 不正な状態を表現不可能にする | 列挙型を使用して有効な状態のみをモデル化する |
unwrap() より ? | エラーを伝播し、ライブラリ/本番コードでパニックしない |
| 検証より解析 | 境界で非構造化データを型付き構造体に変換する |
| 型安全のためのNewtype | 基本型をnewtypeでラップして引数の取り違えを防ぐ |
| ループよりイテレーターを優先 | 宣言的なチェーンはより明確で通常より高速 |
Resultに #[must_use] を使用 | 呼び出し元が戻り値を処理することを保証する |
柔軟な所有権のために Cow を使用 | 借用で十分な場合にアロケーションを避ける |
| 完全マッチング | ビジネスクリティカルな列挙型でワイルドカード _ を使わない |
pub インターフェースを最小化 | 内部APIには pub(crate) を使用 |
避けるべきアンチパターン
rust
// Bad: .unwrap() in production codelet value = map.get("key").unwrap();
// Bad: .clone() to satisfy borrow checker without understanding whylet data = expensive_data.clone();process(&original, &data);
// Bad: Using String when &str sufficesfn greet(name: String) { /* should be &str */ }
// Bad: Box<dyn Error> in libraries (use thiserror instead)fn parse(input: &str) -> Result<Data, Box<dyn std::error::Error>> { todo!() }
// Bad: Ignoring must_use warningslet _ = validate(input); // Silently discarding a Result
// Bad: Blocking in async contextasync fn bad_async() { std::thread::sleep(Duration::from_secs(1)); // Blocks the executor! // Use: tokio::time::sleep(Duration::from_secs(1)).await;}覚えておくこと:コンパイルが通れば、おそらく正しい——ただし unwrap() を避け、unsafe を最小化し、型システムを活用することが前提。


