Rust開発パターン
安全で高性能かつ保守性の高いアプリケーションを構築するための、慣用的なRustのパターンとベストプラクティス。
使用するタイミング
- 新しいRustコードを作成するとき
- Rustコードをレビューするとき
- 既存のRustコードをリファクタリングするとき
- クレートの構造やモジュールのレイアウトを設計するとき
仕組み
このスキルは、6つの主要分野にわたって慣用的なRustの慣習を適用します。所有権と借用によるコンパイル時のデータ競合防止、Result/?によるエラー伝搬(ライブラリではthiserror、アプリケーションでは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!("failed to read config from {path}"))?;
let config: Config = toml::from_str(&content)
.with_context(|| format!("failed to parse config from {path}"))?;
Ok(config)
}
// 悪い例: エラーでパニック
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("record not found: {id}")]
NotFound { id: String },
#[error("connection failed")]
Connection(#[from] std::io::Error),
#[error("invalid data: {0}")]
InvalidData(String),
}
// アプリケーションコード: 柔軟なエラー処理
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コンビネータ
// 良い例: コンビネータチェーン
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!("processed: {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)
}
型安全性のためのNewtypeパターン
// 良い例: 異なる型が引数の混同を防ぐ
struct UserId(u64);
struct OrderId(u64);
fn get_order(user: UserId, order: OrderId) -> Result<Order> {
// 誤ってユーザーIDと注文IDを入れ替えることができない
todo!()
}
// 悪い例: 引数を入れ替えやすい
fn get_order_bad(user_id: u64, order_id: u64) -> Result<Order> {
todo!()
}
構造体とデータモデリング
複雑な構築のためのビルダーパターン
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 poisoned");
*num += 1;
})
}).collect();
for handle in handles {
handle.join().expect("worker thread panicked");
}
メッセージパッシングのためのチャネル
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!("message {i}")).expect("receiver disconnected");
});
}
drop(tx); // 送信側を閉じて、rxイテレータが終了するようにする
for msg in rx {
println!("{msg}");
}
Tokioを使ったAsync
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")
}
// 並行タスクを生成
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!("spawned task panicked: {e}")));
}
results
}
Unsafeコード
Unsafeが許容される場合
// 許容: 文書化された不変条件を持つFFI境界(Rust 2024+)
/// # Safety
/// `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を使用
// 悪い例: Safetyコメントなしでunsafeを使用
// 悪い例: 無関係な型間でのtransmute
モジュールシステムとクレート構造
種類ではなくドメインで整理する
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()より? | エラーを伝搬し、ライブラリ/本番コードでは決してパニックしない |
| 検証するよりパースする | 境界で非構造化データを型付き構造体に変換する |
| 型安全性のためのNewtype | プリミティブをnewtypeでラップして引数の入れ替えを防ぐ |
| ループよりイテレータを優先 | 宣言的なチェーンはより明確で、多くの場合高速 |
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コンテキストでのブロッキング
async fn bad_async() {
std::thread::sleep(Duration::from_secs(1)); // エグゼキュータをブロック!
// 代わりに: tokio::time::sleep(Duration::from_secs(1)).await;
}
覚えておいてください: コンパイルが通れば、おそらく正しいです — ただし、unwrap()を避け、unsafeを最小限に抑え、型システムを活用している場合に限ります。