--- name: rust-patterns description: 慣用的なRustパターン、所有権、エラー処理、トレイト、並行処理、および安全で高性能なアプリケーションを構築するためのベストプラクティス。 origin: ECC --- # Rust 開発パターン 安全で高性能かつ保守性の高いアプリケーションを構築するための慣用的なRustパターンとベストプラクティス。 ## 使用場面 * 新しいRustコードを書く場合 * Rustコードをレビューする場合 * 既存のRustコードをリファクタリングする場合 * クレート構造とモジュールレイアウトを設計する場合 ## 動作原理 このスキルは6つの重要な領域で慣用的なRustの規約を強制する:コンパイル時のデータ競合防止のための所有権と借用、ライブラリでは`thiserror`、アプリケーションでは`anyhow`を使用した`Result`/`?`エラー伝播、不正な状態を表現不可能にする列挙型と完全パターンマッチング、ゼロコスト抽象化のためのトレイトとジェネリクス、`Arc>`、チャンネル、async/awaitによる安全な並行処理、ドメインで整理された最小化された`pub`インターフェース。 ## コア原則 ### 1. 所有権と借用 Rustの所有権システムはコンパイル時にデータ競合とメモリエラーを防ぐ。 ```rust // Good: Pass references when you don't need ownership fn process(data: &[u8]) -> usize { data.len() } // Good: Take ownership only when you need to store or consume fn store(data: Vec) -> Record { Record { payload: data } } // Bad: Cloning unnecessarily to avoid borrow checker fn process_bad(data: &Vec) -> 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 context use anyhow::{Context, Result}; fn load_config(path: &str) -> Result { 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 error fn 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 errors use 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 handling use 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 chain fn find_user_email(users: &[User], id: u64) -> Option { users.iter() .find(|u| u.id == id) .map(|u| u.email.clone()) } // Bad: Deeply nested matching fn find_user_email_bad(users: &[User], id: u64) -> Option { match users.iter().find(|u| u.id == id) { Some(user) => match &user.email { email => Some(email.clone()), }, None => None, } } ``` ## 列挙型とパターンマッチング ### 状態を列挙型としてモデル化する ```rust // Good: Impossible states are unrepresentable 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), } } ``` ### 完全マッチング——ビジネスロジックではワイルドカードを使わない ```rust // Good: Handle every variant explicitly match command { Command::Start => start_service(), Command::Stop => stop_service(), Command::Restart => restart_service(), // Adding a new variant forces handling here } // Bad: Wildcard hides new variants match command { Command::Start => start_service(), _ => {} // Silently ignores Stop, Restart, and future variants } ``` ## トレイトとジェネリクス ### ジェネリックを受け取り、具体的な型を返す ```rust // Good: Generic input, concrete output fn read_all(reader: &mut impl Read) -> std::io::Result> { let mut buf = Vec::new(); reader.read_to_end(&mut buf)?; Ok(buf) } // Good: Trait bounds for multiple constraints fn process(item: T) -> String { format!("processed: {item}") } ``` ### 動的ディスパッチにトレイトオブジェクトを使用する ```rust // Use when you need heterogeneous collections or plugin systems trait Handler: Send + Sync { fn handle(&self, request: &Request) -> Response; } struct Router { handlers: Vec>, } // Use generics when you need performance (monomorphization) fn fast_process(handler: &H, request: &Request) -> Response { handler.handle(request) } ``` ### 型安全のためにNewTypeパターンを使用する ```rust // Good: Distinct types prevent mixing up arguments struct UserId(u64); struct OrderId(u64); fn get_order(user: UserId, order: OrderId) -> Result { // Can't accidentally swap user and order IDs todo!() } // Bad: Easy to swap arguments fn get_order_bad(user_id: u64, order_id: u64) -> Result { todo!() } ``` ## 構造体とデータモデリング ### 複雑な構築にはビルダーパターンを使用する ```rust struct ServerConfig { host: String, port: u16, max_connections: usize, } impl ServerConfig { fn builder(host: impl Into, 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, composable let active_emails: Vec = users.iter() .filter(|u| u.is_active) .map(|u| u.email.clone()) .collect(); // Bad: Imperative accumulation let mut active_emails = Vec::new(); for user in &users { if user.is_active { active_emails.push(user.email.clone()); } } ``` ### 型注釈付きの `collect()` を使用する ```rust // Collect into different types 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(); // Collect Results — short-circuits on first error let parsed: Result, _> = strings.iter().map(|s| s.parse()).collect(); ``` ## 並行処理 ### 共有可変状態には `Arc>` を使用する ```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 { 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 tasks async fn fetch_all(urls: Vec) -> Vec> { 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 bound unsafe { 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 sharing pub(crate) fn validate_input(input: &str) -> bool { !input.is_empty() } // Good: Re-export public API from lib.rs pub mod auth; pub use auth::AuthMiddleware; // Bad: Making everything pub pub fn internal_helper() {} // Should be pub(crate) or private ``` ## ツール統合 ### 基本コマンド ```bash # Build and check cargo build cargo check # Fast type checking without codegen cargo clippy # Lints and suggestions cargo fmt # Format code # Testing cargo test cargo test -- --nocapture # Show println output cargo test --lib # Unit tests only cargo test --test integration # Integration tests only # Dependencies cargo audit # Security audit cargo tree # Dependency tree cargo update # Update dependencies # Performance cargo bench # Run benchmarks ``` ## クイックリファレンス:Rustのイディオム | イディオム | 説明 | |-------|-------------| | 借用、クローンではなく | 所有権が必要でなければ `&T` を渡し、クローンしない | | 不正な状態を表現不可能にする | 列挙型を使用して有効な状態のみをモデル化する | | `unwrap()` より `?` | エラーを伝播し、ライブラリ/本番コードでパニックしない | | 検証より解析 | 境界で非構造化データを型付き構造体に変換する | | 型安全のためのNewtype | 基本型をnewtypeでラップして引数の取り違えを防ぐ | | ループよりイテレーターを優先 | 宣言的なチェーンはより明確で通常より高速 | | Resultに `#[must_use]` を使用 | 呼び出し元が戻り値を処理することを保証する | | 柔軟な所有権のために `Cow` を使用 | 借用で十分な場合にアロケーションを避ける | | 完全マッチング | ビジネスクリティカルな列挙型でワイルドカード `_` を使わない | | `pub` インターフェースを最小化 | 内部APIには `pub(crate)` を使用 | ## 避けるべきアンチパターン ```rust // Bad: .unwrap() in production code let value = map.get("key").unwrap(); // Bad: .clone() to satisfy borrow checker without understanding why let data = expensive_data.clone(); process(&original, &data); // Bad: Using String when &str suffices fn greet(name: String) { /* should be &str */ } // Bad: Box in libraries (use thiserror instead) fn parse(input: &str) -> Result> { todo!() } // Bad: Ignoring must_use warnings let _ = validate(input); // Silently discarding a Result // Bad: Blocking in async context async fn bad_async() { std::thread::sleep(Duration::from_secs(1)); // Blocks the executor! // Use: tokio::time::sleep(Duration::from_secs(1)).await; } ``` **覚えておくこと**:コンパイルが通れば、おそらく正しい——ただし `unwrap()` を避け、`unsafe` を最小化し、型システムを活用することが前提。