Communityコーディング&開発github.com

teneQum/rust-dev

Rustプロジェクトを始めるための初日ガイド。所有権、エラーを値として扱う方法、String対&str、Box/Rc/Arc、anyhow対thiserror、厳選クレートリスト(tokio、serde、axum、sqlx)をカバー。新しいRustプロジェクトを始めるとき、借用チェッカーと格闘しているとき、またはクレートを選ぶときに使用。

rust-dev とは?

このスキルは、Rust開発者が最初のコミットから良いコードを書くための実践的な基盤を提供します。所有権、借用、エラー処理、スマートポインタなど、Rust特有の概念を5つのメンタルモデルで説明し、関数シグネチャ設計の指針を示します。CLI、サービス、ライブラリを問わず、新しいRustプロジェクトを始める際に最適です。tokio、serde、anyhow、clap、axumなどの主要クレートの選び方と設定方法、Cargo.tomlの最小構成、clippyやrustfmtの設定、開発ループ(cargo check/run/test/clippy/fmt)まで網羅しています。Python、JavaScript、Go、Java/C#、C++からの移行者向けに、各言語で陥りがちな落とし穴も詳しく解説。rust-analyzerの必須設定や、最近の安定化機能(let-chains、async closures、assert_matches!など)にも触れ、実践的なRust開発の第一歩をサポートします。

対応~Claude Code~Codex CLI~Cursor
npx skills add https://github.com/tenequm/skills/tree/HEAD/skills/rust-dev

Installed? Explore more コーディング&開発 skills: steipete/bluebubbles, steipete/eightctl, steipete/blucli · View all 6 →

お気に入りのAIに質問する

このエージェントスキルを事前に読み込んだ状態で新しいチャットを開きます。

ドキュメント

Rust開発 - 初日ガイド

最初のコミットからRustアプリをうまく書くための実践的な基礎。教科書ではありません。他の言語との違い、その後のすべてを形作る初日の決断、そしてほとんどの実際のアプリをカバーする少数のクレートに焦点を当てています。

使用すべきタイミング

  • 新しいRustプロジェクト(CLI、サービス、ライブラリ)を始めるとき
  • Python、JavaScript、Go、Java/C#、C++からRustに移行するとき
  • 所有/借用型、スマートポインタ、トレイトオブジェクト対ジェネリクスの選択
  • エラー処理戦略(anyhow対thiserror)を選ぶとき
  • 使用するクレートを決めるとき
  • 最小限で実践的なCargo.toml、clippy、rustfmtを設定するとき

初日セットアップ

# 1. ツールチェーンをインストール(rustupがツールチェーンマネージャ)
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh

# 2. コンポーネントを確認(rustfmtとclippyはstableに同梱、rust-srcはIDE機能を有効化)
rustup component add rustfmt clippy rust-src

# 3. プロジェクトを作成
cargo new my-app          # バイナリ(src/main.rs)
cargo new --lib my-lib    # ライブラリ(src/lib.rs)

# 4. 開発ループ(これら4つを覚える)
cargo check     # 高速型チェック、コード生成なし
cargo run       # ビルドして実行(バイナリ)
cargo test      # ビルドしてテストを実行(doctestを含む)
cargo clippy --all-targets   # リント、テスト/ベンチマークを含む(プッシュ前に実行)
cargo fmt       # フォーマット

# 5. Cargo.tomlを手動で編集せずに依存関係を管理
cargo add tokio --features full
cargo remove tokio
cargo update                 # 既存のセマンティックバージョニング範囲内でCargo.lockを再計算

cargo updateは、Cargo.tomlに既にあるバージョン範囲内でのみ移動します。メジャーバージョン(1.xから2.0)を跨ぐには、Cargo.tomlの編集かcargo add <crate>@2が必要です。

rust-analyzerは必須です。 これはすべてのエディタ(VS Code、Zed、Neovim、Helix、RustRoverは独自エンジンを使用しますが同等)が使用する言語サーバです。VS Codeでは、rust-analyzer拡張機能をインストールし、rust-analyzer.check.commandを"clippy"に設定すると、保存時にリントフィードバックが得られます。

後でファイル監視ツールが必要ですか? cargo install --locked baconを実行し、プロジェクト内でbaconを実行します。初日は必要ありません。

Rustのメンタルモデル:5つのアイデア

Rustは、ほとんどの言語で当たり前とされている2つのもの(ガベージコレクタと例外)を、メモリ、データ競合、エラー処理に関するコンパイル時の保証と引き換えにしています。言語の形状はこのトレードオフから生まれています。

1. 所有権:すべての値にはただ1つの所有者がいる

値を物理的なオブジェクトと考えてください。本、ファイル、ネットワーク接続など。どの瞬間においても、1つの変数がそれを所有しています。以下のことが可能です:

  • ムーブする:let b = a;で所有権がbに移ります。aは使えなくなります。
  • 不変で借用する:&aで他の人に覗かせることができます。複数の読み取りが許可されます。
  • 可変で借用する:&mut aで1人が変更できます。排他的アクセスです。
  • クローンする:a.clone()でディープコピーを作ります。両方がそれぞれのものを保持します。

所有者がスコープを外れると、値はドロップされます(メモリ解放、ファイルクローズ、ロック解放)。GCも手動のfreeもありません。これがRAIIであり、コンパイラによって強制されます。

2. エイリアシング XOR 可変性

どの瞬間においても、データは次のいずれかを持ちます:

  • 1つの可変参照(&mut T)、または
  • 任意の数の不変参照(&T)、

両方は決してありません。この単一のルールがデータ競合とほとんどの使用後解放バグを排除します。借用チェッカーがこれを強制します。借用チェッカーが文句を言うとき、それは言語が難しいのではなく、データの所有権のストーリーが不明確であることを伝えています。

3. エラーは値であり、例外ではない

try/catchはありません。失敗する可能性のある関数はResult<T, E>を返します。有用なものを返さない関数はOption<T>を返します。コンパイラは両方を処理することを強制します。?演算子は、1文字でエラーをコールスタックに伝播させます:

fn read_config() -> Result<Config, anyhow::Error> {
    let text = std::fs::read_to_string("config.toml")?;  // ? = Errなら早期リターン
    let config = toml::from_str(&text)?;
    Ok(config)
}

nullはありません。Option<T>はNoneかSome(value)です。コンパイラはNoneのケースを忘れることを許しません。

4. トレイトはJavaのインターフェースではない

traitは振る舞いを定義します。型はトレイトをimplします。ここまでは似ています。違いは:

  • 静的ディスパッチがデフォルトです。 fn f<T: Display>(x: T)と書くと、コンパイラはfのコピーを、呼び出し元の各具象型Tに対して個別に生成します(単相化、C++テンプレートと同様)。ランタイムオーバーヘッドゼロ。
  • 動的ディスパッチはオプトインで、dyn Trait(通常はBox<dyn Trait>または&dyn Trait)を使用します。呼び出しごとにvtableルックアップが1回。
  • 継承はありません。 トレイトは合成します。型を「拡張」するためにDerefを使いたくなったら、やめて合成かenumを使用してください。
  • 孤児ルール:impl YourTrait for SomeoneElsesTypeまたはimpl SomeoneElsesTrait for YourTypeはできますが、両方が外部のものはできません。これにより依存関係の解決が健全に保たれます。

5. 借用チェッカーは設計の神託

最も一般的な初心者の間違いは、コンパイラエラーを沈黙させるべき障害と見なすことです。そうではありません。ほとんどすべての借用チェックエラーは、誰が何を所有するかに関する実際の問題を明らかにします。行き詰まったときの質問は、「どうやってこれをコンパイルするか」ではなく、「ここで本当に望んでいる所有権の関係は何か」です。エラーを読んでください。コンパイラは異常に情報量が多いです。

関数シグネチャのための3つの質問

関数を書く前に、入力に対して所有、読み取り、変更のどれが必要かを自問してください:

fn consume(s: String)        // 所有:関数が責任を持ち、呼び出し元は失う
fn read(s: &str)             // 読み取り:関数は見るだけ、呼び出し元は保持
fn modify(s: &mut String)    // 変更:関数がその場で変更する

90%のケースで機能するデフォルト:

  • 関数パラメータ:Stringより&str、Vec<T>より&[T]を優先(これらはスライスで、所有型と借用型の両方の呼び出し元を受け入れます)。
  • 関数の戻り値:所有型(String、Vec<T>)を返します。参照を返すことはライフタイムを意味します。必要になるまで避けてください。
  • 構造体フィールド:所有型(String、Vec<T>)を優先。構造体に&strを保存することは、最も一般的な初心者の罠であり、あなたの構造体を保持するすべての型にライフタイム注釈を連鎖させます。

初日決定表

最初に遭遇する選択肢に対する1行の回答。

決定デフォルトもう一方を選ぶとき
String vs &str(構造体フィールド)String本当に理由があり、ライフタイムを理解するまで、&strはほぼ決して使わない
String vs &str(関数パラメータ)&str内部で所有/保存する必要がある場合のみStringを使用
Vec<T> vs &[T](パラメータ)&[T]所有が必要な場合のみVec<T>
Box<T> vs Rc<T> vs Arc<T>Box<T>(単一所有者、ヒープ)スレッド間で共有所有権が必要な場合はArc<T>。デフォルトとしてRc<T>は避け、Arc<T>を使用して非同期に移行するときにリファクタリングしないようにする
RefCell<T> vs Mutex<T>Mutex<T>(またはRwLock<T>)同じ理由:async/スレッドで動作するが、RefCellは動作しない
Option<T> vs Result<T, E>「値がない」場合はOption<T>、「理由があって失敗した」場合はResult<T, E>欠如が呼び出し元が処理すべき意味を持つ場合、Result
dyn Trait vs impl Trait / <T: Trait>ジェネリック(<T: Trait>またはimpl Trait)- 静的ディスパッチ異種コレクション(Vec<Box<dyn Animal>>)が必要な場合、Box<dyn Trait>
アプリケーションコードでのエラーどこでもanyhow::Result<T>-
ライブラリコードでのエラーthiserror派生enum公開ライブラリAPIでBox<dyn Error>は絶対に使わない - 呼び出し元にダウンキャストを強制する
&self vs &mut self vs selfゲッターには&self、セッターには&mut self、ビルダー/消費操作にはself-
モジュール構成ファイルが長くなるまでインラインモジュール、その後分割1モジュール=1ファイルはJava/C#の習慣で、Rustの習慣ではない

早いうちに内面化するイディオム

これらはほぼすべてのRustプログラムに現れます。1週目で学んでください。

?によるエラー伝播。 9行のmatchを1文字で置き換えます。

let body = reqwest::get(url).await?.text().await?;

手動ループよりもイテレータチェーン。 手書きのループと同じ機械語にコンパイルされます(LLVMがクロージャをインライン化)。慣用的なRustは関数型スタイルです。

let active_emails: Vec<String> = users
    .iter()
    .filter(|u| u.active)
    .map(|u| u.email.clone())
    .collect();

matchの網羅性。 enumに新しいバリアントを追加すると、それを処理しないすべてのmatchがコンパイルエラーになります。これを活用してください。これはどんな言語でも最も強力なリファクタリングツールの1つです。

コンパイラにリファクタを任せる。 同じトリックが構造体でも機能します:Defaultなしの必須フィールドを追加すると、既存のすべてのコンストラクタがコンパイルエラーになります - 更新すべきすべてのサイトの無料で網羅的なワークリストです。

if letとlet else は一般的な単一アームのmatchに。

if let Some(name) = user.name { println!("hi {name}"); }

let Some(name) = user.name else { return Err(anyhow!("no name")); };
// この時点から`name`はスコープ内にあり、ネストなし

From / Intoによる型変換。 Fromを実装すれば、Intoを無料で取得。?はFromを使用してエラー型を自動的に変換します。

Option / Resultのコンビネータ。 matchの前に、.map、.and_then、.unwrap_or、.unwrap_or_else、.ok_orに手を伸ばしてください。

派生マクロ。 #[derive(Debug, Clone, PartialEq)]で定型コードの80%を無料で得られます。JSONには#[derive(Serialize, Deserialize)]を追加。

最近の糖衣構文。 知っておく価値のある安定化機能、新しい順:bool::ok_orとok_or_else(1.98)は条件を直接Resultに変換します(ready.ok_or(Error::NotReady)?)。cfg_select!はcfg述語に対するコンパイル時のmatchで、cfg-ifクレートのほとんどの使用を置き換えます。if letガードはmatchアームで動作します(match x { Some(v) if let Ok(n) = v.parse::<i32>() => ... })- 両方とも1.95。assert_matches!とdebug_assert_matches!(1.96)は等価性ではなくパターンに対してアサーションを行い、enumの自然なアサーションです。頻繁に目にし、特殊なものと誤解すべきでない2つの古いもの:let-チェーン(if let Some(x) = a && x > 3 { ... }、エディション2024で1.88から安定)、および非同期クロージャ(async || { ... }、AsyncFnファミリーの境界付き、1.85から安定)。

Xからの移行:ここでつまずく

PythonまたはJavaScriptから:

  • ヒープ値(String、Vecなど)に対するlet b = a;はそれをムーブします。aは使用できなくなります。借用するには&a、コピーするにはa.clone()を使用します。
  • nullはありません。Option<T>が強制されます。
  • 例外はありません。Result<T, E>と?。コンパイラはエラーを無視することを許しません。
  • 継承はありません。合成+トレイト+enum。
  • 変数はデフォルトで不変です。変更するにはmutを追加します。参照も同様:& vs &mut。
  • 名前の再宣言は許可されており、慣用的です:let input = input.trim();は古いバインディングを新しいものでシャドウイングし、異なる型になる可能性があります。これはミューテーションではなく、input2を発明せずに値をその場で変換するRustの方法です。
  • 整数型は明示的であり、インデックスにはusizeが必要です。また、ビルドによってオーバーフローの動作が異なります:デバッグは「プログラムを_ランタイムでパニック_させる整数オーバーフローのチェックを含む」一方、--releaseはそれらをドロップし、「2の補数のラッピング」を実行します。合格するテストスイートでも本番でラップする可能性があるため、デバッグパニックに依存するのではなく、実際に境界に達する可能性のある算術にはchecked_*/saturating_*/wrapping_*を使用してください。strict_addとその仲間(1.91から安定)はすべてのビルドでオーバーフロー時にパニックし、[profile.release]の下のoverflow-checks = trueはデバッグ動作を本番でも維持します。また、整数型間のasは暗黙的に切り捨てられます(300_u32 as u8は44)- 値が収まらない可能性がある場所ではu8::try_from(x)?を使用してください。
  • Stringはインデックス可能ではありません。s[0]はコンパイルされません - 代わりに何を使うべきかについてはreferences/ownership-and-types.mdを参照してください。

Goから:

  • エラーを値として扱う - 同じ発想ですが、if err != nilの代わりに?を使用します。
  • nilはありません。Option<T>。
  • GCもgoroutineもありません:所有権+借用、tokioによるasync/await。非同期モデルは協調的(awaitは明示的な譲歩点)であり、プリエンプティブではありません。
  • interface{}はトレイトになります。静的ディスパッチにはジェネリクスをデフォルトとし、必要な場合のみBox<dyn Trait>を使用します。
  • 静的リンクがデフォルトです。バイナリは大きくなりますが、自己完結型です。
  • panic!はアプリケーションコードの回復不可能なバグのために予約すべきです。Goスタイルの「ログを取って続行」には使用しないでください。

JavaまたはC#から:

  • トレイトはデフォルトで仮想ディスパッチを持つインターフェースではありません。<T: Trait>は単相化されます。dyn Traitはオプトインの動的バージョンです。
  • ヌル参照はありません。Option<T>。
  • 例外はありません。Result<T, E>と?。
  • クラス継承はありません。直和型にはenum、共有動作にはトレイトを使用します。
  • GCはありません:所有権と借用がライフタイムを決定します。Arc<T>はJavaの参照に最も近いものです。
  • ジェネリクスは単相化され、型消去されません。

C++から:

  • RAIIに似ていますが、借用チェッカーがコンパイル時にそれを強制します。
  • コピー/ムーブコンストラクタはありません。Cloneは明示的であり、Copyは安価なビット単位のコピーのためのマーカートレイトです。
  • 安全なコードでは未定義動作はありません(理論上)。
  • &はコンパイル時にチェックされる借用であり、生ポインタではありません。生ポインタ(*const T、*mut T)は存在しますが、デリファレンスにはunsafeが必要です。
  • スマートポインタはBox<T>(unique_ptr)、Rc<T>(shared_ptr、単一スレッド)、Arc<T>(shared_ptr、スレッドセーフ)です。
  • マクロは衛生的です。手続き型マクロ(derive、attribute、function-like)はserde、tokio::mainなどが機能する方法です。

厳選クレートリスト

これらはほとんどの実際のアプリをカバーします。必要に応じて追加してください。すべて必須ではありません。

クレート提供するもの
serde + serde_jsonシリアライゼーション。#[derive(Serialize, Deserialize)]で完了
tokio非同期ランタイム。#[tokio::main]、tokio::spawn、非同期I/O
anyhowアプリケーションエラー型。anyhow::Result<T>、bail!、context()
thiserrorライブラリエラーenum。#[derive(thiserror::Error)]
clapCLI引数解析。#[derive(Parser)]、自動help/version
axumWebフレームワーク。tokio + tower上に構築、#[debug_handler]で良いエラーを提供
sqlx非同期SQL。cargo add sqlx --features runtime-tokio,postgres
reqwestHTTPクライアント。reqwest::get(url).await?.text().await?
tracing構造化ログ。tracing-subscriberで設定。println!の代わりにinfo!、warn!、error!、debug!。非同期と一緒に動作
chrono日付/時刻
uuidUUID
regex正規表現
rand乱数生成

サンプルCargo.toml

ほとんどのCLIまたはサービスのための実用的な出発点:

[package]
name = "my-app"
version = "0.1.0"
edition = "2024"

[dependencies]
anyhow = "1"
clap = { version = "4", features = ["derive"] }
serde = { version = "1", features = ["derive"] }
serde_json = "1"
tokio = { version = "1", features = ["full"] }
# - CLIのみの場合、tokioを削除
# - サービス:axum + sqlx + tower-http + reqwest を追加
# - テスト:ひとまずスキップ、ニーズが生じたら追加

[dev-dependencies]
# 必要になるまでは追加しない

[profile.release]
# デバッグ情報を維持して実用的なバックトレースを取得(サイズに大きな影響なし)
debug = 1
# 本番環境でもオーバーフローパニックを有効化
overflow-checks = true
# デフォルトの3レベルの代わりに単一レベルのLTO(サイズとパフォーマンスに役立つ)
lto = "thin"

clippy設定

.clippy.tomlで、チェックされないことを確認したいルールを設定します:

# ライブラリAPI以外では通常は不要。まだ気にしない
# disallowed-types = ["Box<dyn Error>"]

too-many-arguments-threshold = 10
# 許容引数が多い。毎回構造体を作るよりも悪い
# プロトタイピング段階ではok

clippy.tomlがない場合、clippyはすべてのデフォルトリントを許可して実行します。これは_プロジェクトを作成したとき_に適切です。後で調整します。

rustfmt設定

# rustfmt.toml(オプション、デフォルトはRust 2024スタイル)
# 本当に必要でない限り変更しない。Rustは一貫したフォーマットを重視
# チームが明確な好みを持っている場合のみ設定を追加

# 例:一貫性のあるマクロ呼び出しのために
# normalize_comments = true

初期テスト戦略

  • ドキュメントテスト(///ブロック内のコード)は無料でミニテストを提供します。cargo testで実行されます。使用してください。
  • ユニットテストはテスト中のモジュールと同じファイルの#[cfg(test)] mod tests { ... }に配置します。
  • 統合テストはtests/ディレクトリに配置します。cargo testが自動的に見つけます。各ファイルは独自のクレートとしてコンパイルされます。
  • 統合テストのためのcommonモジュール:tests/common/mod.rsに配置します(tests/common.rsは_テストとして実行_されるため避けてください)。
  • ほとんどの場合、assert_eq!、assert!、Result<()>を返すテスト(#[test] fn it_works() -> Result<()> { ... })で十分です。
  • 必要な場合のみモック。代わりにトレイトを使用してテストでスタブを注入します。

エディタ設定

VS Code

拡張機能のsettings.json(またはワークスペース設定):

{
    "rust-analyzer.check.command": "clippy",
    "editor.formatOnSave": true,
    "editor.inlayHints.enabled": "on"
}

Zed

Zedはコードアクションと言語サーバの設定を含むRustサポートを内蔵しています。

Neovim

rust-analyzer用のmason.nvim / nvim-lspconfig、フォーマット用のconform.nvim、リント用のnvim-lint。

トラブルシューティング

問題最初に試すこと
「cannot borrow x as mutable more than once at a time」xを別々のスコープで使用しているか確認。スコープを導入({ let y = &mut x; ... })するか、借用を並べ替える
「cannot move out of x which is behind a shared reference」クローンするか、共有所有権(Arc)にリファクタリングする
「expected &str, found String」&s(暗黙の参照外し、自動逆参照は文字列では機能しません - s.as_str()を使うか、*sが機能する場合があります)。通常は、文字列リテラルに対して&strを使用
「expected String, found &str」s.to_string()またはs.to_owned()
「lifetime mismatch」おそらく構造体に&strを保存しています。Stringを使用する
「the trait Iterator is not implemented for MyType」IntoIterator(.into_iter()で消費)と.iter()(コレクションへの参照)の違いを確認
「use of moved value: x」xは別の場所に移動されました。借用する(&x)か、クローンする(x.clone())
「Box<dyn Trait> cannot be sent between threads safely」コードにSendを追加(Box<dyn Trait + Send>)するか、Arc<Mutex<T>>に変更する。このエラーは非同期コードで、クロージャがSendでないものをキャプチャした場合に頻繁に発生します
「recursion in async fn requires Box」async_recursionクレートを追加するか(#[async_recursion])、手動でPin<Box<dyn Future>>を返す
「async block may outlive the current function」tokio::spawnされたタスクがスコープ外になる参照をキャプチャしています。.awaitするか、クローン/所有する

次に読むべきもの

初日以降のための優先順位付きロードマップ:

  1. The Rust Book(chapter 4-6, 8-10, 13, 15, 16)- 章はあなたの進む方向に沿っている
  2. std::iterのドキュメント - イテレータアダプタを理解することは第二言語を学ぶようなもの。Iteratorトレイトのドキュメントを読んで、関連するすべてのメソッドを理解する
  3. Effective Rust - 新しい言語を学ぶときにこれに相当するものは貴重
  4. Rust by Example - 9割のケースをカバーするコード例
  5. Rust API Guidelines - ライブラリを書く場合のみ

覚えておいてください:Rustはバグのクラスを排除する最初の主流言語であり、その代償は学習曲線です。その曲線は急ですが短いです。コンパイラと戦わず、耳を傾けてください。

Individual skills in this repo

This repo contains 4 individual skills — each has its own dedicated page.

関連スキル