Rustをゼロから学ぶ
プログラミング未経験者がRustを習得し、生成AIを活用してWebアプリを構築できるようになるための教材へようこそ。
対象者
- シェル操作(ディレクトリ移動など)ができる方
- プログラミングは未経験の方
- コンパイルの概念を知らない方
学習の流れ
Phase 0: 基盤構築(コンピュータとプログラミングの基礎)
↓
Phase 1: Rust基礎(変数、制御フロー、関数)
↓
Phase 2: 所有権とメモリ(Rust最重要概念)
↓
Phase 3: 構造化プログラミング(struct、enum、trait)
↓
Phase 4: エコシステムと実践(Cargo、非同期)
↓
Phase 5: Webアプリ開発(Axum、データベース)
↓
最終プロジェクト: ブックマーク管理Webアプリ
学習の進め方
- 各Phaseのドキュメントを順番に読む
- 各ドキュメント末尾の**確認テスト(5問)**で理解度をチェック
- 実践プロジェクトでコードを書いて動かす
- わからないことは生成AIに質問(活用ガイドライン参照)
確認テストについて
各ドキュメントの末尾に5問の確認テストがあります:
| 問題タイプ | 問数 | 内容 |
|---|---|---|
| 概念理解(選択式) | 2問 | 基本概念の定着確認 |
| コード読解 | 1問 | コードの動作を予測 |
| エラー修正 | 1問 | コンパイルエラーを修正 |
| 実践課題 | 1問 | 小さなコードを書く |
進捗管理機能: 各テストの「理解できた」チェックボックスをクリックすると、進捗がブラウザに保存されます。進捗ダッシュボードで全体の学習状況を確認できます。
各Phaseの概要
Phase 0: 基盤構築
コンピュータの仕組み、プログラミングの概念、Rustのセットアップ、Hello Worldまで。
Phase 1: Rust基礎
変数と型、演算子、制御フロー、関数、コレクション、エラー処理の基本。
Phase 2: 所有権とメモリ(最重要)
Rust独自の概念。スタックとヒープ、所有権、参照と借用、ライフタイム。
Phase 3: 構造化プログラミング
構造体、列挙型、トレイト、ジェネリクス、モジュール、高度なエラー処理。
Phase 4: エコシステムと実践
Cargoの応用、外部クレートの選定、テスト、ドキュメント、非同期プログラミング。
Phase 5: Webアプリ開発
HTTP/REST基礎、Axumフレームワーク、データベース連携、認証、デプロイ。
実践プロジェクト
| プロジェクト | Phase | 学習ポイント |
|---|---|---|
| p0_hello_rust | 0 | Rustセットアップ、コンパイル |
| p1_guessing_game | 1 | 入力、乱数、ループ |
| p2_calculator | 1 | 関数、match、エラー処理 |
| p3_text_processor | 2 | String、&str、所有権 |
| p4_task_cli | 3 | struct、enum、JSON、モジュール |
| p5_weather_cli | 4 | 非同期、API呼び出し、テスト |
| p6_bookmark_api | 5 | Axum、SQLx、認証 |
| final_bookmark_app | 全て | フルスタックWebアプリ |
困ったときは
さあ、始めましょう!
最初のドキュメントは:
進捗ダッシュボード
学習の進捗状況を確認できます。クイズの結果はブラウザに自動保存されます。
全体の進捗
クイズ完了: 0 / 34
Phase別進捗
Phase 0: 基盤構築
0 / 5 クイズ
Phase 1: Rust基礎
0 / 6 クイズ
Phase 2: 所有権
0 / 5 クイズ
Phase 3: 構造化
0 / 6 クイズ
Phase 4: エコシステム
0 / 5 クイズ
Phase 5: Web開発
0 / 7 クイズ
学習のヒント
- 各ドキュメントの末尾にある確認テスト(5問)に取り組みましょう
- 全問正解すると🎉マークが表示されます
- 進捗データは「エクスポート」でJSON形式でバックアップできます
- 別のブラウザに移行する場合は「インポート」で復元できます
コンピュータの基本構造
プログラミングを始める前に、コンピュータがどのように動いているかを理解しましょう。
コンピュータの3つの主要部品
コンピュータは大きく分けて3つの部品で構成されています。
┌─────────────────────────────────────────────────┐
│ コンピュータ │
│ │
│ ┌─────────┐ ┌─────────┐ ┌─────────────────┐ │
│ │ CPU │ │ メモリ │ │ ストレージ │ │
│ │ (頭脳) │ │ (作業台) │ │ (倉庫) │ │
│ └─────────┘ └─────────┘ └─────────────────┘ │
│ ↑ ↑ ↑ │
│ └────────────┴──────────────┘ │
│ データのやり取り │
└─────────────────────────────────────────────────┘
1. CPU(Central Processing Unit)- 頭脳
役割: 計算と命令の実行
CPUは「中央演算処理装置」と呼ばれ、コンピュータの頭脳です。
- プログラムの命令を1つずつ読み取って実行する
- 足し算、引き算などの計算を行う
- 条件に応じて次に何をするか判断する
例え: 料理人
料理人が「レシピ(プログラム)」を読んで、「材料を切る」「火にかける」などの指示を1つずつ実行するイメージです。
2. メモリ(RAM)- 作業台
役割: 一時的なデータの保管
メモリは「作業台」のようなものです。
- CPUが今まさに使っているデータを置く場所
- 電源を切ると中身は消える(揮発性)
- 高速にアクセスできる
例え: 料理の作業台
料理人が今使っている材料や調理中の料理を置く作業台です。作業が終わったら片付けます。
3. ストレージ(SSD/HDD)- 倉庫
役割: 永続的なデータの保管
ストレージは「倉庫」のようなものです。
- ファイルやプログラムを保存する場所
- 電源を切っても中身は消えない(不揮発性)
- メモリより遅いがたくさん保存できる
例え: 食材倉庫
使わない材料や保存食を置いておく倉庫です。必要なときに取り出します。
実際の動作の流れ
あなたがプログラムを実行するとき、こんな流れで動きます:
1. ストレージからプログラムを読み込む
[ストレージ] ──読み込み──→ [メモリ]
2. CPUがメモリ上のプログラムを実行
[メモリ] ←──命令取得──→ [CPU]
←──結果保存──→
3. 結果をストレージに保存(必要な場合)
[メモリ] ──書き込み──→ [ストレージ]
なぜこれを知る必要があるのか?
Rustを学ぶ上で、特にメモリの理解が重要です。
Rustは「メモリ安全」を重視する言語で、プログラムがメモリをどう使うかを細かく制御します。Phase 2で「所有権」という概念を学びますが、それはメモリの管理に関係しています。
今は「メモリは一時的な作業台」ということを覚えておいてください。
まとめ
| 部品 | 役割 | 特徴 |
|---|---|---|
| CPU | 計算・実行 | 頭脳、命令を処理 |
| メモリ | 一時保管 | 高速、電源OFFで消える |
| ストレージ | 永続保管 | 大容量、電源OFFでも残る |
確認テスト
Q1. CPUの役割として正しいものはどれ?
Q2. メモリ(RAM)の特徴として正しいものはどれ?
Q3. プログラム実行の正しい順序はどれ?
Q4. 「メモリはファイルを永続的に保存する場所」という説明について正しいものはどれ?
Q5. ストレージの特徴として正しいものはどれ?
次のドキュメント: 02_what_is_programming.md
プログラミングとは何か
プログラムとは「コンピュータへの指示書」
プログラムとは、コンピュータに「何をしてほしいか」を伝える指示書です。
人間が書く指示書 コンピュータが理解
(ソースコード) → (機械語) → 実行
人間は日本語や英語で考えますが、コンピュータは「0」と「1」の組み合わせ(機械語)しか理解できません。
プログラミング言語は、人間が書きやすい形式で指示を書き、それをコンピュータが理解できる形に変換するための仕組みです。
料理のレシピに例えると
プログラムは料理のレシピに似ています。
カレーのレシピ(人間向け):
1. 玉ねぎを切る
2. 肉を炒める
3. 野菜を加える
4. 水を入れて煮込む
5. ルーを入れる
6. 完成!
プログラム(コンピュータ向け):
fn main() {
println!("玉ねぎを切る");
println!("肉を炒める");
println!("野菜を加える");
println!("水を入れて煮込む");
println!("ルーを入れる");
println!("完成!");
}
どちらも「手順を順番に書いたもの」という点で同じです。
ソースコードとは
プログラマーが書くプログラムの文章をソースコードと呼びます。
- 人間が読み書きできるテキスト形式
- 特定のプログラミング言語のルールに従って書く
- ファイルとして保存する(Rustの場合は
.rsという拡張子)
Rustのソースコード例:
// hello.rs(ファイル名)
fn main() {
println!("Hello, World!");
}
プログラミングの基本的な流れ
┌──────────────┐
│ 1. 書く │ ソースコードを書く(hello.rs)
└──────┬───────┘
↓
┌──────────────┐
│ 2. 変換 │ コンピュータが理解できる形に変換
└──────┬───────┘
↓
┌──────────────┐
│ 3. 実行 │ プログラムを動かす
└──────┬───────┘
↓
┌──────────────┐
│ 4. 結果確認 │ 期待通りに動いたか確認
└──────────────┘
この「変換」の部分が、次のドキュメントで説明する「コンパイル」です。
プログラミング言語の種類
世の中には多くのプログラミング言語があります。
| 言語 | 特徴 | 主な用途 |
|---|---|---|
| Python | 読みやすく書きやすい | AI、データ分析 |
| JavaScript | ブラウザで動く | Webサイト |
| Java | 大規模システム向け | 企業システム |
| Rust | 安全で高速 | システム、Web、組み込み |
どの言語も「コンピュータへの指示を書く」という目的は同じですが、書き方や得意分野が異なります。
なぜRustを学ぶのか
Rustには以下の特徴があります:
- 安全性: メモリに関するバグを防ぐ仕組みがある
- 高速性: C/C++と同等の実行速度
- 現代的: 最近の言語設計のベストプラクティスを取り入れている
- 成長中: 多くの企業が採用し、需要が増えている
特に「安全性」の部分は、Rust独自の「所有権」という仕組みで実現されています(Phase 2で詳しく学びます)。
エラーは味方
プログラミングを始めると、たくさんのエラーに遭遇します。
エラーは失敗ではありません。学習の機会です。
Rustは特に親切なエラーメッセージを出してくれる言語です。エラーメッセージを読む習慣をつけましょう。
error[E0384]: cannot assign twice to immutable variable `x`
--> src/main.rs:3:5
|
2 | let x = 5;
| -
| |
| first assignment to `x`
| help: consider making this binding mutable: `mut x`
3 | x = 6;
| ^^^^^ cannot assign twice to immutable variable
このエラーは「xという変数に2回値を代入しようとしているけど、変更不可(immutable)だよ。mutをつけると変更可能になるよ」と教えてくれています。
まとめ
- プログラムはコンピュータへの指示書
- ソースコードは人間が書くプログラムの文章
- エラーは学習の機会、恐れずに読もう
- Rustは安全で高速な現代的言語
確認テスト
Q1. ソースコードとは何か?
Q2. プログラミングの基本的な流れとして正しい順番は?
Q3. 以下のRustコードは何を出力する?fn main() { println!("こんにちは"); println!("Rust"); }
println!は画面に文字を出力する命令です。2回呼び出しているので、「こんにちは」と「Rust」が別々の行に出力されます。
Q4. 以下のエラーメッセージは何を伝えている?error: expected `;`, found `}`
;)が必要なのに、}が見つかった」というエラーです。Rustでは文の終わりにセミコロンが必要です。
Q5. プログラミングの本質として最も適切な説明は?
次のドキュメント: 03_compile_vs_interpret.md
コンパイルとインタプリタ
プログラミング言語には、ソースコードを実行する方法が2種類あります。
2つの実行方式
コンパイル方式(Rustはこちら)
┌─────────────┐ ┌─────────────┐ ┌─────────────┐
│ ソースコード │ ──→ │ コンパイラ │ ──→ │ 実行ファイル │
│ (hello.rs) │ │ (rustc) │ │ (hello) │
└─────────────┘ └─────────────┘ └─────────────┘
│
↓
┌─────────────┐
│ 何度でも実行 │
└─────────────┘
特徴:
- 実行前にソースコード全体を機械語に変換(コンパイル)
- 一度変換すると、何度でも高速に実行できる
- 変換時にエラーを検出できる
例え: 本の翻訳
英語の本を日本語に翻訳して出版するようなもの。翻訳には時間がかかるが、一度翻訳すれば何度でも読める。
インタプリタ方式
┌─────────────┐ ┌─────────────┐
│ ソースコード │ ──→ │インタプリタ │ ──→ 実行
│ (hello.py) │ │ (python) │
└─────────────┘ └─────────────┘
│
↓
1行ずつ変換しながら実行
特徴:
- 実行しながら1行ずつ変換(逐次解釈)
- すぐに実行できる
- 実行速度はコンパイル方式より遅い
例え: 同時通訳
英語のスピーチを同時通訳するようなもの。すぐに理解できるが、通訳しながらなので時間がかかる。
比較表
| 観点 | コンパイル方式 | インタプリタ方式 |
|---|---|---|
| 変換タイミング | 実行前に全体を変換 | 実行中に1行ずつ |
| 実行速度 | 高速 | 比較的遅い |
| エラー検出 | 実行前に検出 | 実行時に検出 |
| 開発サイクル | 変換に時間がかかる | すぐ試せる |
| 代表的な言語 | Rust, C, C++, Go | Python, JavaScript, Ruby |
Rustがコンパイル言語である意味
Rustはコンパイル方式を採用しています。これには大きなメリットがあります。
1. 実行前にエラーを発見できる
fn main() {
let x = 5;
x = 6; // エラー!変更不可の変数を変更しようとしている
}
このコードはコンパイル時にエラーになります。プログラムを動かす前に問題がわかります。
インタプリタ方式だと、実際にこの行が実行されるまでエラーがわかりません。
2. 高速に実行できる
コンパイル済みのプログラムは機械語になっているので、CPUが直接実行できます。
実行速度のイメージ:
Rust (コンパイル): ████████████████████ 100%
Python (インタプリタ): ███ 10-30%
3. 配布が簡単
コンパイルして作られた実行ファイルは、そのまま他の人に渡せます。受け取った人はRustをインストールしなくても実行できます。
コンパイルの流れ(Rust)
┌─────────────┐
│ hello.rs │ ソースコードを書く
└──────┬──────┘
│ rustc hello.rs(または cargo build)
↓
┌─────────────┐
│ コンパイル │
│ ・文法チェック│
│ ・型チェック │
│ ・所有権チェック│
│ ・最適化 │
└──────┬──────┘
│
↓
┌─────────────┐
│ hello │ 実行ファイル(機械語)
└──────┬──────┘
│ ./hello
↓
┌─────────────┐
│ 実行結果 │ Hello, World!
└─────────────┘
Rustのコンパイラは特にチェックが厳しいことで知られています。
- 文法チェック:書き方が正しいか
- 型チェック:データの種類が合っているか
- 所有権チェック:メモリの使い方が安全か
これらのチェックのおかげで、実行時のバグを減らせます。
コンパイルエラーは友達
コンパイル時にエラーが出ると、最初は戸惑うかもしれません。
しかし、実行時にエラーが出るより、コンパイル時にエラーが出る方がはるかに良いのです。
なぜなら:
- 実行時エラー:本番環境でユーザーに影響する可能性
- コンパイルエラー:開発中に気づける、ユーザーには影響しない
Rustのコンパイラはエラーメッセージが親切です。エラーを恐れず、メッセージをよく読みましょう。
まとめ
- コンパイル方式: 実行前に全体を変換、高速、エラーを早期発見
- インタプリタ方式: 1行ずつ変換しながら実行、すぐ試せる
- Rustはコンパイル方式: 安全性と速度を両立
- コンパイルエラーは味方: 実行前に問題を発見できる
確認テスト
Q1. コンパイル方式の特徴として正しいものはどれ?
Q2. Rustがコンパイル言語であるメリットとして正しいものはどれ?
Q3. rustc hello.rs を実行すると何が起こる?
rustcはRustコンパイラです。hello.rsをコンパイルして、実行ファイルhelloを生成します。
Q4. 「インタプリタ方式は実行前に全体をチェックするのでエラーを早期に発見できる」この説明は正しい?
Q5. 大量のデータを高速に処理するプログラムに適した方式は?
次のドキュメント: 04_rust_setup.md
Rust開発環境の構築
Rustでプログラミングを始めるための環境を整えましょう。
必要なもの
- Rust本体(rustup経由でインストール)
- テキストエディタ(VS Codeを推奨)
Rustのインストール
macOS / Linux の場合
ターミナルを開いて、以下のコマンドを実行します:
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
インストール中に選択肢が表示されたら、1(default)を選んでEnterを押します。
インストール完了後、ターミナルを再起動するか、以下を実行:
source $HOME/.cargo/env
Windows の場合
- https://rustup.rs にアクセス
- 「RUSTUP-INIT.EXE」をダウンロードして実行
- 画面の指示に従ってインストール
インストールの確認
ターミナル(またはコマンドプロンプト)で以下を実行:
rustc --version
バージョンが表示されれば成功です:
rustc 1.XX.X (xxxxxxxx 2024-XX-XX)
インストールされるツール
rustupをインストールすると、以下のツールが使えるようになります:
| ツール | 役割 |
|---|---|
rustup | Rustのバージョン管理 |
rustc | Rustコンパイラ |
cargo | パッケージマネージャ・ビルドツール |
rustup
Rust本体のバージョンを管理するツールです。
# Rustを最新版に更新
rustup update
# インストールされているバージョンを確認
rustup show
rustc
Rustコンパイラです。ソースコードを実行ファイルに変換します。
# 単一ファイルをコンパイル
rustc hello.rs
cargo(重要)
Rustの「パッケージマネージャ」兼「ビルドツール」です。実際の開発ではcargoを使います。
# 新しいプロジェクトを作成
cargo new my_project
# プロジェクトをビルド
cargo build
# プロジェクトを実行
cargo run
# コードをチェック(コンパイルはしない)
cargo check
エディタの設定(VS Code)
VS Codeのインストール
- https://code.visualstudio.com からダウンロード
- インストーラを実行
Rust拡張機能のインストール
VS Codeを開いて:
- 左のサイドバーで拡張機能アイコン(四角が4つ)をクリック
- 「rust-analyzer」を検索
- 「Install」をクリック
rust-analyzerは以下の機能を提供します:
- コード補完
- エラーのリアルタイム表示
- 型情報の表示
- コードジャンプ
動作確認
環境が正しく設定されたか確認しましょう。
1. プロジェクトを作成
cargo new hello_rust
cd hello_rust
2. 生成されたファイルを確認
hello_rust/
├── Cargo.toml # プロジェクトの設定ファイル
└── src/
└── main.rs # ソースコード
Cargo.tomlの中身:
[package]
name = "hello_rust"
version = "0.1.0"
edition = "2021"
[dependencies]
src/main.rsの中身:
fn main() {
println!("Hello, world!");
}
3. 実行
cargo run
以下が表示されれば成功です:
Compiling hello_rust v0.1.0 (/path/to/hello_rust)
Finished dev [unoptimized + debuginfo] target(s) in 0.XXs
Running `target/debug/hello_rust`
Hello, world!
トラブルシューティング
「command not found: cargo」と表示される
ターミナルを再起動するか、以下を実行:
source $HOME/.cargo/env
それでも動かない場合は、~/.cargo/binがPATHに含まれているか確認:
echo $PATH | grep cargo
VS Codeでrust-analyzerが動かない
- VS Codeを再起動
cargo buildを一度実行してから確認- rust-analyzer拡張機能を再インストール
コンパイルが非常に遅い
初回のコンパイルは依存関係のダウンロードとビルドがあるため遅いです。2回目以降は速くなります。
まとめ
- rustup: Rust本体のインストール・更新
- cargo: プロジェクト管理・ビルド(これを主に使う)
- VS Code + rust-analyzer: 快適な開発環境
環境構築は最初だけです。一度設定すれば、あとはコードを書くことに集中できます。
確認テスト
Q1. Rustプロジェクトの作成・ビルド・実行に主に使うツールはどれ?
cargo new)、ビルド(cargo build)、実行(cargo run)などを行います。
Q2. cargo new my_project を実行すると何が作成される?
Q3. cargo new hello → cd hello → cargo run を実行すると何が表示される?
Q4. error: could not find Cargo.toml というエラーの原因は?
cd プロジェクト名でプロジェクトディレクトリに移動してから実行しましょう。
Q5. Rustのバージョンを最新に更新するコマンドは?
rustup updateでRustを最新版に更新できます。
次のドキュメント: 05_first_program.md
はじめてのRustプログラム
実際にRustのプログラムを書いて動かしてみましょう。
Hello, World!
プログラミング学習の伝統として、最初に書くプログラムは「Hello, World!」を表示するものです。
プロジェクトの作成
cargo new hello_world
cd hello_world
コードを見てみよう
src/main.rs を開くと、以下のコードが書かれています:
fn main() {
println!("Hello, world!");
}
たった3行!これがRustプログラムの最小構成です。
1行ずつ解説
fn main() {
fn: 「function(関数)」の略。関数を定義するキーワードmain: 関数の名前。mainはプログラムの開始地点(): 引数リスト。今は空っぽ{: 関数の中身の始まり
#![allow(unused)]
fn main() {
println!("Hello, world!");
}
println!: 画面に文字を出力するマクロ(!がついているのがマクロの印)"Hello, world!": 表示する文字列;: 文の終わり
#![allow(unused)]
fn main() {
}
}
}: 関数の中身の終わり
実行
cargo run
出力:
Hello, world!
コードを変更してみよう
表示する文字を変える
src/main.rsを以下のように変更:
fn main() {
println!("こんにちは、Rust!");
}
保存して実行:
cargo run
出力:
こんにちは、Rust!
複数行を表示する
fn main() {
println!("1行目");
println!("2行目");
println!("3行目");
}
出力:
1行目
2行目
3行目
わざとエラーを起こしてみよう
エラーメッセージを読む練習をしましょう。
エラー1: セミコロン忘れ
fn main() {
println!("Hello, world!") // セミコロンがない!
}
cargo run
エラー:
error: expected `;`, found `}`
--> src/main.rs:3:1
|
2 | println!("Hello, world!")
| - help: add `;` here
3 | }
| ^ unexpected token
読み方:
expected;, found}``:セミコロンを期待したのに}が見つかったhelp: add;here:ここにセミコロンを追加して
エラー2: 閉じカッコ忘れ
fn main() {
println!("Hello, world!"; // 閉じカッコがない!
}
エラー:
error: expected `)`, found `;`
--> src/main.rs:2:29
|
2 | println!("Hello, world!";
| ^ expected `)`
エラー3: クォート忘れ
fn main() {
println!(Hello, world!); // クォートがない!
}
エラー:
error: expected `(`, found `Hello`
--> src/main.rs:2:14
|
2 | println!(Hello, world!);
| ^^^^^ expected `(`
プログラムの構造を理解する
fn main() { // ← プログラムの入口(エントリーポイント)
// ここに処理を書く
println!("処理1");
println!("処理2");
} // ← プログラムの終わり
Rustプログラムは必ずmain関数から実行が始まります。
コメントの書き方
fn main() {
// これは1行コメント
println!("Hello!"); // 行末コメント
/*
これは
複数行
コメント
*/
}
コメントはプログラムの実行に影響しません。メモとして使います。
cargo runとcargo build
cargo run
コンパイル + 実行を一度に行います。
cargo run
cargo build
コンパイルのみ行います。実行ファイルはtarget/debug/に作られます。
cargo build
./target/debug/hello_world
cargo check
コンパイルエラーがないかチェックします。実行ファイルは作りません。
cargo check
使い分け:
- 開発中は
cargo check(速い) - 動作確認は
cargo run - 配布用は
cargo build --release(最適化される)
実践プロジェクト
projects/p0_hello_rust/ に移動して、以下を試してみましょう。
- 自分の名前を表示するプログラムを書く
- 好きな言葉を3行表示するプログラムを書く
- わざとエラーを起こして、エラーメッセージを読む
まとめ
fn main() { }はプログラムの入口println!()で文字を表示;で文を終わるcargo runでコンパイル+実行- エラーメッセージを読む習慣をつけよう
確認テスト
Q1. Rustプログラムの実行が始まる場所はどこ?
main関数から実行が始まります。これを「エントリーポイント」と呼びます。
Q2. println! の ! は何を意味する?
! がついているものはマクロです。マクロは関数より柔軟な処理ができます。
Q3. 以下のコードの出力は?fn main() { println!("A"); println!("B"); println!("C"); }
println!は呼び出すたびに1行出力し、自動的に改行します。上から順に実行されます。
Q4. println!("Hello, Rust!) のエラーは何?
" で始まり " で終わる必要があります。また、文の終わりには ; が必要です。
Q5. コンパイル+実行を一度に行うコマンドは?
cargo runはコンパイルと実行を一度に行います。cargo buildはコンパイルのみ、cargo checkはエラーチェックのみです。
Phase 0 完了!
おめでとうございます!Phase 0を完了しました。
学んだこと:
- コンピュータの基本構造(CPU、メモリ、ストレージ)
- プログラミングとは何か
- コンパイルとインタプリタの違い
- Rust開発環境の構築
- はじめてのRustプログラム
次のPhase: Phase 1: Rust基礎
変数と型
プログラムでデータを扱う基本、「変数」と「型」について学びます。
変数とは
変数は「データを入れておく箱」です。
fn main() {
let age = 25; // ageという変数に25を入れる
println!("年齢: {}", age);
}
出力:
年齢: 25
変数の宣言
#![allow(unused)]
fn main() {
let 変数名 = 値;
}
let: 変数を作るキーワード変数名: 好きな名前をつけられる(ルールあり)=: 値を代入する値: 変数に入れるデータ
変数名のルール
#![allow(unused)]
fn main() {
// OK
let age = 25;
let user_name = "太郎";
let totalCount = 100;
let _temp = 0;
// NG
let 123abc = 10; // 数字で始まってはいけない
let my-name = ""; // ハイフンは使えない
let let = 5; // 予約語は使えない
}
慣習: Rustではsnake_case(小文字とアンダースコア)が推奨されます。
#![allow(unused)]
fn main() {
let user_name = "太郎"; // 推奨
let userName = "太郎"; // 動くけど警告が出る
}
型(Type)とは
型は「データの種類」です。
#![allow(unused)]
fn main() {
let age = 25; // 整数型
let height = 170.5; // 浮動小数点型
let name = "太郎"; // 文字列型
let is_student = true; // 真偽型
}
Rustは静的型付け言語で、すべての変数には型があります。
基本的な型
整数型
| 型 | サイズ | 範囲 |
|---|---|---|
i8 | 8bit | -128 〜 127 |
i16 | 16bit | -32,768 〜 32,767 |
i32 | 32bit | 約-21億 〜 約21億(デフォルト) |
i64 | 64bit | とても大きい |
u8 | 8bit | 0 〜 255 |
u16 | 16bit | 0 〜 65,535 |
u32 | 32bit | 0 〜 約42億 |
u64 | 64bit | とても大きい |
i: 符号あり(マイナスも扱える)u: 符号なし(0以上のみ)
#![allow(unused)]
fn main() {
let age: i32 = 25;
let count: u32 = 100;
}
通常は型を書かなくてもRustが推論してくれます:
#![allow(unused)]
fn main() {
let age = 25; // i32と推論される
}
浮動小数点型
| 型 | サイズ | 説明 |
|---|---|---|
f32 | 32bit | 単精度 |
f64 | 64bit | 倍精度(デフォルト) |
#![allow(unused)]
fn main() {
let height = 170.5; // f64と推論
let weight: f32 = 65.0; // 明示的にf32
}
真偽型(bool)
#![allow(unused)]
fn main() {
let is_student = true;
let is_adult = false;
}
true(真)またはfalse(偽)の2値のみ。
文字型(char)
#![allow(unused)]
fn main() {
let c = 'A';
let emoji = '😀';
let kanji = '漢';
}
シングルクォート'で囲む。1文字だけ。
文字列型
#![allow(unused)]
fn main() {
let name = "太郎"; // &str型(文字列スライス)
let greeting = String::from("こんにちは"); // String型
}
ダブルクォート"で囲む。文字列は後で詳しく学びます。
型注釈
型を明示的に書くことを型注釈といいます:
#![allow(unused)]
fn main() {
let age: i32 = 25;
let height: f64 = 170.5;
let name: &str = "太郎";
let is_student: bool = true;
}
通常はRustが推論してくれるので省略できますが、書いた方がわかりやすい場合もあります。
不変(immutable)と可変(mutable)
デフォルトは不変
Rustの変数はデフォルトで不変です。
fn main() {
let x = 5;
x = 6; // エラー!
}
エラー:
error[E0384]: cannot assign twice to immutable variable `x`
可変にするには mut
fn main() {
let mut x = 5; // mutをつける
println!("x = {}", x);
x = 6; // OK!
println!("x = {}", x);
}
出力:
x = 5
x = 6
なぜデフォルトが不変なのか?
- バグを減らせる:意図しない変更を防ぐ
- コードが読みやすい:変わらないことが保証される
- 並行処理で安全:複数の処理が同時にアクセスしても安全
「変更する必要がある」ときだけ mut をつける習慣をつけましょう。
シャドーイング
同じ名前の変数を再度宣言することをシャドーイングといいます:
fn main() {
let x = 5;
let x = x + 1; // 新しいxで古いxを「覆い隠す」
let x = x * 2;
println!("x = {}", x); // 12
}
mutとの違い:
- シャドーイング:新しい変数を作る。型も変えられる
mut:同じ変数の値を変える。型は変えられない
#![allow(unused)]
fn main() {
// シャドーイング:型を変えられる
let spaces = " "; // &str
let spaces = spaces.len(); // usize(型が変わった)
// mut:型は変えられない
let mut spaces = " ";
spaces = spaces.len(); // エラー!型が違う
}
定数
変わらない値には定数を使います:
const MAX_POINTS: u32 = 100_000;
fn main() {
println!("最大ポイント: {}", MAX_POINTS);
}
定数の特徴:
constキーワードを使う- 型注釈が必須
- 大文字とアンダースコアで命名(
SCREAMING_SNAKE_CASE) - プログラム全体で使える
まとめ
| 概念 | 説明 |
|---|---|
| 変数 | データを入れる箱。letで作る |
| 型 | データの種類(i32, f64, bool, charなど) |
| 不変 | デフォルト。変更できない |
| 可変 | mutをつけると変更可能 |
| シャドーイング | 同名の変数を再宣言 |
| 定数 | constで宣言。変更不可 |
確認テスト
Q1. Rustの変数のデフォルトの性質は?
mut キーワードをつけます。
Q2. 以下の型のうち、符号なし整数型はどれ?
uで始まる型は符号なし(unsigned)整数型で、0以上の値のみを扱います。iで始まる型は符号あり(signed)で、マイナスの値も扱えます。
Q3. 以下のコードの出力は?let x = 5; let x = x + 1; let x = x * 2; println!("{}", x);
Q4. let count = 0; count = count + 1; このコードのエラーを修正するには?
mut キーワードを追加します。不変の変数に再代入しようとしたのがエラーの原因です。
Q5. 「身長170.5cm」を格納するのに最適な型は?
次のドキュメント: 02_operators.md
演算子
計算や比較を行うための「演算子」について学びます。
算術演算子
数値の計算に使います。
fn main() {
let a = 10;
let b = 3;
println!("足し算: {} + {} = {}", a, b, a + b); // 13
println!("引き算: {} - {} = {}", a, b, a - b); // 7
println!("掛け算: {} * {} = {}", a, b, a * b); // 30
println!("割り算: {} / {} = {}", a, b, a / b); // 3(整数同士は切り捨て)
println!("剰余: {} % {} = {}", a, b, a % b); // 1(余り)
}
| 演算子 | 意味 | 例 |
|---|---|---|
+ | 足し算 | 5 + 3 → 8 |
- | 引き算 | 5 - 3 → 2 |
* | 掛け算 | 5 * 3 → 15 |
/ | 割り算 | 5 / 3 → 1 |
% | 剰余(余り) | 5 % 3 → 2 |
整数の割り算に注意
整数同士の割り算は小数点以下が切り捨てられます:
fn main() {
let x = 10 / 3; // 3(3.333...ではない)
println!("{}", x);
}
小数点以下が必要な場合は浮動小数点数を使います:
fn main() {
let x = 10.0 / 3.0; // 3.333...
println!("{}", x);
}
型が違うと計算できない
fn main() {
let a: i32 = 10;
let b: f64 = 3.0;
// let c = a + b; // エラー!型が違う
let c = a as f64 + b; // aをf64に変換してから計算
println!("{}", c); // 13.0
}
比較演算子
2つの値を比較して、trueまたはfalseを返します。
fn main() {
let a = 5;
let b = 3;
println!("{} == {} は {}", a, b, a == b); // false
println!("{} != {} は {}", a, b, a != b); // true
println!("{} > {} は {}", a, b, a > b); // true
println!("{} < {} は {}", a, b, a < b); // false
println!("{} >= {} は {}", a, b, a >= b); // true
println!("{} <= {} は {}", a, b, a <= b); // false
}
| 演算子 | 意味 | 例 |
|---|---|---|
== | 等しい | 5 == 5 → true |
!= | 等しくない | 5 != 3 → true |
> | より大きい | 5 > 3 → true |
< | より小さい | 5 < 3 → false |
>= | 以上 | 5 >= 5 → true |
<= | 以下 | 5 <= 3 → false |
=と==の違い
#![allow(unused)]
fn main() {
let x = 5; // 代入(xに5を入れる)
x == 5 // 比較(xが5と等しいかチェック)
}
初心者がよく間違えるポイントです!
論理演算子
bool値を組み合わせます。
fn main() {
let a = true;
let b = false;
println!("AND: {} && {} = {}", a, b, a && b); // false
println!("OR: {} || {} = {}", a, b, a || b); // true
println!("NOT: !{} = {}", a, !a); // false
}
| 演算子 | 意味 | 説明 |
|---|---|---|
&& | AND(かつ) | 両方trueならtrue |
|| | OR(または) | どちらかtrueならtrue |
! | NOT(否定) | trueとfalseを反転 |
真理値表
AND (&&)
| a | b | a && b |
|---|---|---|
| true | true | true |
| true | false | false |
| false | true | false |
| false | false | false |
OR (||)
| a | b | a || b |
|---|---|---|
| true | true | true |
| true | false | true |
| false | true | true |
| false | false | false |
NOT (!)
| a | !a |
|---|---|
| true | false |
| false | true |
実用例
fn main() {
let age = 20;
let has_id = true;
// 20歳以上 かつ IDを持っている
let can_enter = age >= 20 && has_id;
println!("入場可能: {}", can_enter); // true
// 65歳以上 または 12歳未満
let is_discount = age >= 65 || age < 12;
println!("割引対象: {}", is_discount); // false
}
複合代入演算子
計算と代入を同時に行います。
fn main() {
let mut x = 10;
x += 5; // x = x + 5 と同じ
println!("x += 5 → {}", x); // 15
x -= 3; // x = x - 3 と同じ
println!("x -= 3 → {}", x); // 12
x *= 2; // x = x * 2 と同じ
println!("x *= 2 → {}", x); // 24
x /= 4; // x = x / 4 と同じ
println!("x /= 4 → {}", x); // 6
x %= 4; // x = x % 4 と同じ
println!("x %= 4 → {}", x); // 2
}
| 演算子 | 意味 |
|---|---|
+= | 足して代入 |
-= | 引いて代入 |
*= | 掛けて代入 |
/= | 割って代入 |
%= | 剰余を代入 |
演算子の優先順位
数学と同じで、掛け算・割り算が足し算・引き算より先に計算されます。
fn main() {
let result = 2 + 3 * 4; // 2 + 12 = 14
println!("{}", result);
let result = (2 + 3) * 4; // 5 * 4 = 20
println!("{}", result);
}
迷ったらカッコを使いましょう。読みやすさも向上します。
まとめ
| 種類 | 演算子 | 用途 |
|---|---|---|
| 算術 | +, -, *, /, % | 数値計算 |
| 比較 | ==, !=, >, <, >=, <= | 値の比較 |
| 論理 | &&, ||, ! | 条件の組み合わせ |
| 複合代入 | +=, -=, *=, /=, %= | 計算して代入 |
確認テスト
Q1. 10 / 3 の結果は?(両方とも整数)
10.0 / 3.0 のように浮動小数点数を使います。
Q2. true && false の結果は?
&&) は両方が true の場合のみ true を返します。片方でも false なら結果は false です。
Q3. 以下のコードの出力は?let mut x = 5; x += 3; x *= 2; println!("{}", x);
Q4. let a: i32 = 10; let b: f64 = 2.5; let c = a + b; のエラーを修正するには?
as キーワードで型変換(キャスト)を行います。
Q5. 「18歳以上 かつ 身長150cm以上」を判定する条件式は?
&&、「以上」は >= を使います。両方の条件を同時に満たす必要があります。
次のドキュメント: 03_control_flow.md
制御フロー
プログラムの流れを制御する「条件分岐」と「繰り返し」について学びます。
if式(条件分岐)
条件によって処理を分けます。
基本形
fn main() {
let age = 20;
if age >= 18 {
println!("成人です");
}
}
if-else
fn main() {
let age = 15;
if age >= 18 {
println!("成人です");
} else {
println!("未成年です");
}
}
if-else if-else
fn main() {
let score = 75;
if score >= 90 {
println!("優");
} else if score >= 70 {
println!("良");
} else if score >= 50 {
println!("可");
} else {
println!("不可");
}
}
条件は必ずbool型
fn main() {
let number = 3;
// if number { // エラー!numberはi32でboolではない
if number != 0 { // OK
println!("ゼロではない");
}
}
他の言語(JavaScript、Pythonなど)では if number が動くこともありますが、Rustでは必ず bool 型が必要です。
ifは「式」である
Rustでは if は値を返す「式」です。
fn main() {
let condition = true;
let number = if condition { 5 } else { 6 };
println!("number = {}", number); // 5
}
注意: 両方のブロックが同じ型を返す必要があります。
#![allow(unused)]
fn main() {
// エラー!型が違う
// let number = if condition { 5 } else { "six" };
}
loop(無限ループ)
loop は明示的に止めるまで繰り返します。
fn main() {
let mut count = 0;
loop {
count += 1;
println!("カウント: {}", count);
if count >= 5 {
break; // ループを抜ける
}
}
}
breakで値を返す
fn main() {
let mut counter = 0;
let result = loop {
counter += 1;
if counter == 10 {
break counter * 2; // 20を返す
}
};
println!("結果: {}", result); // 20
}
while(条件付きループ)
条件が true の間、繰り返します。
fn main() {
let mut number = 3;
while number != 0 {
println!("{}!", number);
number -= 1;
}
println!("発射!");
}
出力:
3!
2!
1!
発射!
for(範囲ループ)
最もよく使うループです。
範囲を使った繰り返し
fn main() {
// 1から4まで(5は含まない)
for i in 1..5 {
println!("{}", i);
}
}
出力:
1
2
3
4
範囲の種類
fn main() {
// 1..5 : 1, 2, 3, 4(5を含まない)
for i in 1..5 {
print!("{} ", i);
}
println!();
// 1..=5 : 1, 2, 3, 4, 5(5を含む)
for i in 1..=5 {
print!("{} ", i);
}
println!();
}
配列の繰り返し
fn main() {
let numbers = [10, 20, 30, 40, 50];
for n in numbers {
println!("{}", n);
}
}
インデックス付きで繰り返し
fn main() {
let fruits = ["りんご", "みかん", "バナナ"];
for (index, fruit) in fruits.iter().enumerate() {
println!("{}: {}", index, fruit);
}
}
出力:
0: りんご
1: みかん
2: バナナ
breakとcontinue
break(ループを抜ける)
fn main() {
for i in 1..10 {
if i == 5 {
break; // ループ終了
}
println!("{}", i);
}
}
出力:
1
2
3
4
continue(次の繰り返しへ)
fn main() {
for i in 1..10 {
if i % 2 == 0 {
continue; // 偶数はスキップ
}
println!("{}", i);
}
}
出力:
1
3
5
7
9
match(パターンマッチング)
複数の条件を整理して書けます。
fn main() {
let number = 3;
match number {
1 => println!("一"),
2 => println!("二"),
3 => println!("三"),
_ => println!("その他"), // _ はそれ以外すべて
}
}
複数の値をまとめる
fn main() {
let number = 5;
match number {
1 | 2 | 3 => println!("小さい"), // 1, 2, 3のどれか
4..=6 => println!("中くらい"), // 4〜6の範囲
_ => println!("大きい"),
}
}
matchは式
fn main() {
let number = 3;
let description = match number {
1 => "一つ",
2 => "二つ",
_ => "たくさん",
};
println!("{}", description);
}
まとめ
| 構文 | 用途 |
|---|---|
if | 条件分岐 |
loop | 無限ループ(breakで終了) |
while | 条件が真の間ループ |
for | 範囲や配列を繰り返し |
match | パターンマッチング |
break | ループを抜ける |
continue | 次の繰り返しへスキップ |
確認テスト
Q1. for i in 1..5 で i が取る値は?
1..5 は1から始まり、5を含みません(5未満)。5を含めたい場合は 1..=5 を使います。
Q2. continue の動作として正しいのは?
continue は現在の繰り返しの残りの処理をスキップし、次の繰り返しに進みます。break はループを完全に終了します。
Q3. 以下のコードの出力は?let mut sum = 0; for i in 1..=3 { sum += i; } println!("{}", sum);
1..=3 は1, 2, 3を含むので、合計は 1 + 2 + 3 = 6 です。
Q4. if number { ... } でnumberがi32型の場合、どう修正すべき?
if の条件は必ず bool 型でなければなりません。比較演算子を使って明示的にbool値を作成します。
Q5. 3の倍数を判定する条件式として正しいのは?
% を使って、3で割った余りが0かどうかを判定します。余りが0なら3の倍数です。
次のドキュメント: 04_functions.md
関数
処理をまとめて再利用可能にする「関数」について学びます。
関数とは
関数は「処理をまとめた部品」です。
fn main() {
greet(); // 関数を呼び出す
}
fn greet() {
println!("こんにちは!");
}
なぜ関数を使うのか
- 再利用: 同じ処理を何度も書かなくて済む
- 整理: コードを意味のある単位に分割
- テスト: 部品ごとにテストしやすい
関数の定義
#![allow(unused)]
fn main() {
fn 関数名() {
// 処理
}
}
fn: 関数を定義するキーワード関数名: snake_case で命名(): 引数リスト{}: 関数の本体
命名規則
#![allow(unused)]
fn main() {
fn calculate_total() { } // OK: snake_case
fn calculateTotal() { } // 動くが警告が出る
}
Rustでは関数名に snake_case(小文字とアンダースコア)を使います。
引数
関数にデータを渡せます。
fn main() {
greet("太郎");
greet("花子");
}
fn greet(name: &str) {
println!("こんにちは、{}さん!", name);
}
出力:
こんにちは、太郎さん!
こんにちは、花子さん!
引数の書き方
#![allow(unused)]
fn main() {
fn 関数名(引数名: 型) {
// 処理
}
}
型注釈は必須です。省略できません。
複数の引数
fn main() {
print_sum(5, 3);
}
fn print_sum(a: i32, b: i32) {
println!("{} + {} = {}", a, b, a + b);
}
戻り値
関数は値を返すことができます。
fn main() {
let result = add(5, 3);
println!("結果: {}", result);
}
fn add(a: i32, b: i32) -> i32 {
a + b // セミコロンなし = この値を返す
}
戻り値の書き方
#![allow(unused)]
fn main() {
fn 関数名(引数) -> 戻り値の型 {
戻り値 // セミコロンなし
}
}
->: 戻り値の型を示す- 最後の式の値が返される(セミコロンをつけない)
return を使う方法
#![allow(unused)]
fn main() {
fn add(a: i32, b: i32) -> i32 {
return a + b; // 明示的にreturn
}
}
return を使うと、関数の途中でも値を返して終了できます:
#![allow(unused)]
fn main() {
fn absolute(n: i32) -> i32 {
if n < 0 {
return -n; // ここで終了
}
n // nが0以上の場合
}
}
式と文
Rustではセミコロンの有無が重要です。
fn main() {
let x = {
let y = 3;
y + 1 // セミコロンなし → これが式の値になる
};
println!("{}", x); // 4
}
fn main() {
let x = {
let y = 3;
y + 1; // セミコロンあり → 値を返さない(()を返す)
};
// xは () 型になる
}
| 式(Expression) | 文(Statement) | |
|---|---|---|
| セミコロン | なし | あり |
| 値を返す | はい | いいえ |
| 例 | 5 + 3, x * 2 | let x = 5;, x = 3; |
関数を使った例
例1: 面積の計算
fn main() {
let width = 10;
let height = 5;
let area = calculate_area(width, height);
println!("面積: {}", area);
}
fn calculate_area(width: i32, height: i32) -> i32 {
width * height
}
例2: 偶数判定
fn main() {
for i in 1..=10 {
if is_even(i) {
println!("{}は偶数", i);
}
}
}
fn is_even(n: i32) -> bool {
n % 2 == 0
}
例3: 最大値を求める
fn main() {
let max = maximum(10, 25);
println!("最大値: {}", max);
}
fn maximum(a: i32, b: i32) -> i32 {
if a > b {
a
} else {
b
}
}
関数の設計指針
1. 一つのことをうまくやる
#![allow(unused)]
fn main() {
// 良い例:一つのことに集中
fn calculate_tax(price: i32) -> i32 {
price / 10
}
fn calculate_total(price: i32, tax: i32) -> i32 {
price + tax
}
// 悪い例:複数のことをやっている
fn calculate_and_print_total(price: i32) {
let tax = price / 10;
let total = price + tax;
println!("税込: {}", total);
}
}
2. 適切な名前をつける
#![allow(unused)]
fn main() {
// 良い例:何をするかわかる
fn calculate_area(width: i32, height: i32) -> i32 { ... }
fn is_valid_email(email: &str) -> bool { ... }
// 悪い例:わかりにくい
fn calc(a: i32, b: i32) -> i32 { ... }
fn check(s: &str) -> bool { ... }
}
3. 引数は少なめに
引数が多すぎると使いにくくなります。3〜4個を超えたら構造体の使用を検討。
まとめ
| 要素 | 説明 |
|---|---|
fn | 関数を定義 |
| 引数 | 関数に渡すデータ。型注釈必須 |
-> | 戻り値の型を示す |
| 戻り値 | 最後の式(セミコロンなし)が返る |
return | 明示的に値を返す |
確認テスト
Q1. 関数の戻り値について正しいのは?
Q2. 関数の引数について正しいのは?
let では型推論が働きますが、関数の引数では明示が必須です。
Q3. double(triple(2)) の結果は?(double: n*2, triple: n*3)
Q4. fn add(a: i32, b: i32) -> i32 { a + b; } のエラーを修正するには?
return a + b; とします。
Q5. FizzBuzz問題で「15」は何と出力される?
次のドキュメント: 05_collections_basic.md
コレクション基礎
複数のデータをまとめて扱う「配列」「タプル」「Vec」について学びます。
配列(Array)
同じ型のデータを固定長で並べたものです。
配列の作成
fn main() {
let numbers = [1, 2, 3, 4, 5];
println!("{:?}", numbers);
}
型と長さの指定
#![allow(unused)]
fn main() {
let numbers: [i32; 5] = [1, 2, 3, 4, 5];
// 型 長さ
}
同じ値で初期化
#![allow(unused)]
fn main() {
let zeros = [0; 5]; // [0, 0, 0, 0, 0]
}
要素へのアクセス
fn main() {
let numbers = [10, 20, 30, 40, 50];
println!("1番目: {}", numbers[0]); // 10
println!("3番目: {}", numbers[2]); // 30
println!("長さ: {}", numbers.len()); // 5
}
インデックスは0から始まります!
配列の繰り返し
fn main() {
let numbers = [1, 2, 3, 4, 5];
for n in numbers {
println!("{}", n);
}
}
配列の制限
- 長さが固定: 作成後に増減できない
- 同じ型のみ: 異なる型は混ぜられない
#![allow(unused)]
fn main() {
// エラー:長さが違う
// let arr: [i32; 5] = [1, 2, 3];
// エラー:型が混在
// let arr = [1, "two", 3];
}
タプル(Tuple)
異なる型のデータをまとめられます。
タプルの作成
fn main() {
let person = ("太郎", 25, 170.5);
println!("{:?}", person);
}
型の指定
#![allow(unused)]
fn main() {
let person: (&str, i32, f64) = ("太郎", 25, 170.5);
}
要素へのアクセス
fn main() {
let person = ("太郎", 25, 170.5);
println!("名前: {}", person.0); // 太郎
println!("年齢: {}", person.1); // 25
println!("身長: {}", person.2); // 170.5
}
分解(Destructuring)
fn main() {
let person = ("太郎", 25, 170.5);
let (name, age, height) = person;
println!("{}さんは{}歳、身長{}cm", name, age, height);
}
関数の複数戻り値として使う
fn main() {
let (min, max) = min_max(5, 10, 3);
println!("最小: {}, 最大: {}", min, max);
}
fn min_max(a: i32, b: i32, c: i32) -> (i32, i32) {
let min = if a < b && a < c { a } else if b < c { b } else { c };
let max = if a > b && a > c { a } else if b > c { b } else { c };
(min, max)
}
Vec(ベクター)
可変長の配列です。要素を追加・削除できます。
Vecの作成
fn main() {
// 空のVec
let mut numbers: Vec<i32> = Vec::new();
// マクロを使った作成
let numbers = vec![1, 2, 3, 4, 5];
println!("{:?}", numbers);
}
要素の追加
fn main() {
let mut numbers = Vec::new();
numbers.push(1);
numbers.push(2);
numbers.push(3);
println!("{:?}", numbers); // [1, 2, 3]
}
要素の削除
fn main() {
let mut numbers = vec![1, 2, 3, 4, 5];
let last = numbers.pop(); // 最後の要素を取り出す
println!("取り出した値: {:?}", last); // Some(5)
println!("残り: {:?}", numbers); // [1, 2, 3, 4]
}
要素へのアクセス
fn main() {
let numbers = vec![10, 20, 30, 40, 50];
// インデックスでアクセス(範囲外でパニック)
println!("{}", numbers[0]); // 10
// getでアクセス(範囲外でNone)
match numbers.get(10) {
Some(n) => println!("{}", n),
None => println!("範囲外"),
}
}
Vecの繰り返し
fn main() {
let numbers = vec![1, 2, 3, 4, 5];
for n in &numbers { // 参照で繰り返す
println!("{}", n);
}
println!("Vecはまだ使える: {:?}", numbers);
}
Vecの便利なメソッド
fn main() {
let numbers = vec![1, 2, 3, 4, 5];
println!("長さ: {}", numbers.len()); // 5
println!("空?: {}", numbers.is_empty()); // false
println!("含む?: {}", numbers.contains(&3)); // true
}
比較表
| 特徴 | 配列 | タプル | Vec |
|---|---|---|---|
| 長さ | 固定 | 固定 | 可変 |
| 型 | 同じ型のみ | 異なる型OK | 同じ型のみ |
| アクセス | arr[i] | tuple.i | vec[i] or vec.get(i) |
| 用途 | 固定サイズのデータ | 複数の戻り値 | 動的なリスト |
使い分け
配列を使う場面
- データ数が決まっている
- 変更する必要がない
#![allow(unused)]
fn main() {
let days = ["月", "火", "水", "木", "金", "土", "日"];
}
タプルを使う場面
- 異なる型のデータをまとめる
- 関数から複数の値を返す
#![allow(unused)]
fn main() {
fn get_user() -> (&str, i32) {
("太郎", 25)
}
}
Vecを使う場面
- データ数が変わる
- 要素を追加・削除する
#![allow(unused)]
fn main() {
let mut todo_list = vec!["買い物", "掃除"];
todo_list.push("洗濯");
}
まとめ
| コレクション | 作成 | 長さ | 型の制限 |
|---|---|---|---|
| 配列 | [1, 2, 3] | 固定 | 同じ型のみ |
| タプル | (1, "a", true) | 固定 | 異なる型OK |
| Vec | vec![1, 2, 3] | 可変 | 同じ型のみ |
確認テスト
Q1. 配列とVecの違いは?
push や pop で要素を追加・削除できます。
Q2. タプルの要素にアクセスする方法は?
[] は使いません。
Q3. 以下のコードの出力は?let mut v = vec![1, 2, 3]; v.push(4); v.pop(); v.push(5); println!("{:?}", v);
Q4. let numbers = vec![1, 2, 3]; numbers.push(4); のエラーを修正するには?
push を呼ぶには変数が mut である必要があります。let mut numbers = vec![1, 2, 3]; と変更します。
Q5. 空のVecに1から10までの偶数を追加した場合、合計はいくつ?
次のドキュメント: 06_error_handling_intro.md
エラーハンドリング入門
プログラムで起こりうるエラーを適切に処理する方法を学びます。
Rustのエラー処理の考え方
多くの言語では「例外」を使いますが、Rustは型システムでエラーを扱います。
他の言語: 成功 or 例外(突然プログラムが止まる)
Rust: Result<成功, 失敗> を返す(呼び出し側が処理を決める)
これにより、エラーを「忘れて無視する」ことが難しくなります。
Option型:値があるかないか
Optionは「値があるかもしれないし、ないかもしれない」を表します。
Option の定義(イメージ)
#![allow(unused)]
fn main() {
enum Option<T> {
Some(T), // 値がある
None, // 値がない
}
}
使用例
fn main() {
let numbers = vec![1, 2, 3];
// インデックス1の要素を取得
match numbers.get(1) {
Some(n) => println!("見つかった: {}", n),
None => println!("見つからない"),
}
// インデックス10の要素を取得(範囲外)
match numbers.get(10) {
Some(n) => println!("見つかった: {}", n),
None => println!("見つからない"),
}
}
出力:
見つかった: 2
見つからない
なぜ Option を使うのか
#![allow(unused)]
fn main() {
// 他の言語でよくあるパターン(危険)
// let value = array[10]; // 範囲外 → クラッシュ!
// Rustのパターン(安全)
let value = numbers.get(10); // Option<&i32> を返す
// 必ず Some/None を処理する必要がある
}
Option の便利なメソッド
fn main() {
let some_number = Some(5);
let no_number: Option<i32> = None;
// unwrap_or: Noneの場合のデフォルト値
println!("{}", some_number.unwrap_or(0)); // 5
println!("{}", no_number.unwrap_or(0)); // 0
// is_some / is_none
println!("{}", some_number.is_some()); // true
println!("{}", no_number.is_none()); // true
}
Result型:成功か失敗か
Resultは「処理が成功したか失敗したか」を表します。
Result の定義(イメージ)
#![allow(unused)]
fn main() {
enum Result<T, E> {
Ok(T), // 成功(Tは成功時の値)
Err(E), // 失敗(Eはエラー情報)
}
}
使用例
fn main() {
let result = divide(10, 2);
match result {
Ok(value) => println!("結果: {}", value),
Err(e) => println!("エラー: {}", e),
}
let result = divide(10, 0);
match result {
Ok(value) => println!("結果: {}", value),
Err(e) => println!("エラー: {}", e),
}
}
fn divide(a: i32, b: i32) -> Result<i32, String> {
if b == 0 {
Err(String::from("ゼロで割ることはできません"))
} else {
Ok(a / b)
}
}
出力:
結果: 5
エラー: ゼロで割ることはできません
ファイル操作の例
use std::fs;
fn main() {
let result = fs::read_to_string("hello.txt");
match result {
Ok(content) => println!("内容: {}", content),
Err(e) => println!("読み込みエラー: {}", e),
}
}
ファイルが存在しない場合、Errが返されます。
unwrapとexpect(注意が必要)
unwrap
SomeまたはOkの値を取り出します。NoneやErrの場合はパニック(プログラム停止)。
fn main() {
let some_value = Some(5);
let value = some_value.unwrap(); // 5
println!("{}", value);
// let none_value: Option<i32> = None;
// let value = none_value.unwrap(); // パニック!
}
expect
unwrapと同じですが、エラーメッセージを指定できます。
fn main() {
let result: Result<i32, &str> = Err("何か問題が発生");
// let value = result.expect("致命的なエラー"); // パニック!メッセージ付き
}
unwrap/expectを使っていい場面
- 学習中・プロトタイプ: 素早く動かしたいとき
- 絶対に失敗しない場合: 論理的に失敗しないことが保証されるとき
#![allow(unused)]
fn main() {
// OK: "42"は必ず数値に変換できる
let number: i32 = "42".parse().unwrap();
}
本番コードでは避ける
#![allow(unused)]
fn main() {
// 避ける
let content = fs::read_to_string("config.txt").unwrap();
// 推奨
let content = fs::read_to_string("config.txt")
.expect("設定ファイルの読み込みに失敗"); // せめてメッセージを
// より良い: エラーを伝播
fn read_config() -> Result<String, std::io::Error> {
fs::read_to_string("config.txt")
}
}
if letによる簡潔な処理
特定のパターンだけ処理したい場合に便利です。
fn main() {
let some_value = Some(5);
// matchを使う場合
match some_value {
Some(n) => println!("値: {}", n),
None => {} // 何もしない
}
// if letを使う場合(簡潔)
if let Some(n) = some_value {
println!("値: {}", n);
}
}
まとめ
| 型 | 用途 | バリアント |
|---|---|---|
Option<T> | 値の有無 | Some(T), None |
Result<T, E> | 成功/失敗 | Ok(T), Err(E) |
| メソッド | 動作 | 注意 |
|---|---|---|
unwrap() | 値を取り出す | 失敗でパニック |
expect("msg") | 値を取り出す | 失敗でメッセージ付きパニック |
unwrap_or(default) | 値またはデフォルト | 安全 |
is_some() / is_ok() | 成功かチェック | 安全 |
確認テスト
Q1. Option<T>のNoneは何を表す?
Noneは「値がない」ことを表します。エラーではなく、単に値が存在しない状態です。エラーを表すにはResult<T, E>のErrを使います。
Q2. unwrap()を使う際のリスクは?
unwrap()はSomeやOkの値を取り出しますが、NoneやErrの場合はプログラムがパニックして停止します。
Q3. 以下のコードの出力は?let numbers = vec![10, 20, 30]; let value = numbers.get(5).unwrap_or(&0); println!("{}", value);
numbers.get(5)はインデックス5が範囲外なのでNoneを返します。unwrap_or(&0)はNoneの場合にデフォルト値&0を返すので、結果は0です。
Q4. unwrap()を使わずに安全にOptionを処理する方法として適切でないのは?
Option型はasでキャストできません。match、if let、unwrap_orなどを使って安全に値を取り出します。
Q5. safe_divide(10, 0)がNoneを返す関数を実装する場合、正しいのは?
Option<i32>を返す場合、成功時はSome(値)、失敗時はNoneを返します。Resultを使う場合はC)のような形になります。
Phase 1 完了!
おめでとうございます!Phase 1を完了しました。
学んだこと:
- 変数と型(let、mut、基本型)
- 演算子(算術、比較、論理)
- 制御フロー(if、loop、while、for、match)
- 関数(引数、戻り値、式と文)
- コレクション(配列、タプル、Vec)
- エラーハンドリング入門(Option、Result)
次のPhase: Phase 2: 所有権とメモリ
Phase 2はRust最重要の概念「所有権」を学びます。ここが理解できればRustマスターへの道が開けます!
スタックとヒープ
所有権を理解するために、まずメモリの仕組みを学びます。
メモリの2つの領域
プログラムが使うメモリには主に2つの領域があります。
┌─────────────────────────────────────────┐
│ メモリ │
│ ┌─────────────┐ ┌─────────────────┐ │
│ │ スタック │ │ ヒープ │ │
│ │ (Stack) │ │ (Heap) │ │
│ │ │ │ │ │
│ │ 高速・固定 │ │ 柔軟・可変 │ │
│ │ サイズ限定 │ │ サイズ自由 │ │
│ └─────────────┘ └─────────────────┘ │
└─────────────────────────────────────────┘
スタック(Stack)
特徴
- LIFO(Last In, First Out):最後に入れたものを最初に取り出す
- 高速:メモリの場所が決まっている
- 固定サイズ:コンパイル時にサイズがわかるデータのみ
イメージ:本の積み重ね
↓ push(追加)
┌─────┐
│ 3 │ ← 最後に追加
├─────┤
│ 2 │
├─────┤
│ 1 │ ← 最初に追加
└─────┘
↑ pop(取り出し)は上から
スタックに置かれるデータ
fn main() {
let x: i32 = 42; // スタック(サイズ固定:4バイト)
let y: f64 = 3.14; // スタック(サイズ固定:8バイト)
let z: bool = true; // スタック(サイズ固定:1バイト)
let arr: [i32; 3] = [1, 2, 3]; // スタック(サイズ固定:12バイト)
}
これらはコンパイル時にサイズがわかるので、スタックに置かれます。
ヒープ(Heap)
特徴
- 動的:実行時にサイズが決まるデータを置ける
- 柔軟:大きなデータや可変長データを扱える
- 間接アクセス:ポインタ経由でアクセス
イメージ:倉庫と伝票
スタック(伝票) ヒープ(倉庫)
┌─────────┐ ┌─────────────────┐
│ ptr ────────────────→│ "Hello, Rust!" │
│ len: 13 │ └─────────────────┘
│ cap: 13 │
└─────────┘
ヒープに置かれるデータ
fn main() {
let s = String::from("Hello"); // ヒープに文字列データ
let v = vec![1, 2, 3, 4, 5]; // ヒープに配列データ
}
StringやVecは実行時にサイズが変わる可能性があるので、ヒープに置かれます。
Stringのメモリレイアウト
#![allow(unused)]
fn main() {
let s = String::from("hello");
}
スタック ヒープ
┌─────────────┐ ┌───┬───┬───┬───┬───┐
│ ptr ─────────────────────→│ h │ e │ l │ l │ o │
├─────────────┤ └───┴───┴───┴───┴───┘
│ len: 5 │ (5バイトのデータ)
├─────────────┤
│ capacity: 5 │
└─────────────┘
- ptr: ヒープ上のデータへのポインタ
- len: 現在の長さ
- capacity: 確保済みの容量
なぜ2つの領域があるのか
スタックの限界
#![allow(unused)]
fn main() {
// これはコンパイルエラー(サイズが不明)
// let s: str = ???; // strのサイズは実行時まで不明
}
スタックには「コンパイル時にサイズがわかるもの」しか置けません。
ヒープの必要性
fn main() {
let mut s = String::from("Hello");
s.push_str(", World!"); // 文字列が伸びる
println!("{}", s);
}
Stringは後から長さが変わるので、柔軟なヒープに置く必要があります。
速度の違い
アクセス速度の比較(イメージ):
スタック: ████████████████████ 100%
ヒープ: ██████████ 50%(ポインタ経由のため)
スタックは「次のデータの場所」が決まっているので高速です。 ヒープは「ポインタをたどる」必要があるため、少し遅くなります。
コピーとムーブ
スタックのデータ:コピー
fn main() {
let x = 5;
let y = x; // xの値をコピー
println!("x = {}, y = {}", x, y); // 両方使える
}
スタックのデータは小さいので、そのままコピーされます。
ヒープのデータ:ムーブ
fn main() {
let s1 = String::from("hello");
let s2 = s1; // s1からs2にムーブ
// println!("{}", s1); // エラー!s1は無効
println!("{}", s2); // OK
}
ヒープのデータは大きい可能性があるので、所有権が移動します。 これが次のドキュメントで学ぶ「所有権」の核心です。
まとめ
| 特徴 | スタック | ヒープ |
|---|---|---|
| サイズ | 固定(コンパイル時に決定) | 可変(実行時に決定) |
| 速度 | 高速 | 比較的遅い |
| データ例 | i32, f64, bool, 配列 | String, Vec |
| 代入時 | コピー | ムーブ(所有権移動) |
確認テスト
Q1. スタックに置かれるデータの特徴として正しいものはどれですか?
Q2. String型がヒープにデータを置く理由として正しいものはどれですか?
Stringはpush_strなどで後から長さが変わる可能性があります。そのため、柔軟にサイズを変更できるヒープに置かれます。
Q3. 以下のコードで、ヒープにデータが確保される変数はどれですか?let a: i32 = 10; let b = String::from("hi"); let c: [i32; 3] = [1,2,3];
a(i32)とc(固定長配列)はスタックのみに置かれます。b(String)はスタックにポインタ・長さ・容量を持ち、実際の文字データはヒープに置かれます。
Q4. 以下のコードがコンパイルエラーになる理由は何ですか?let s1 = String::from("hello"); let s2 = s1; println!("{}", s1);
Stringはヒープにデータを持つため、代入時にコピーではなくムーブが発生します。s1の所有権がs2に移動した後、s1は無効となり使用できません。s1.clone()を使えば明示的にコピーできます。
Q5. let x = 5; let y = x;の実行後、xとyの状態として正しいものはどれですか?
i32はCopy型なので、代入時にスタック上で値がコピーされます。xとyはそれぞれ別々のメモリ位置に同じ値5を持ち、両方とも有効です。
次のドキュメント: 02_ownership.md
所有権(Ownership)
Rust最重要の概念です。これを理解すればRustの大部分が理解できます。
所有権とは
Rustでは、すべての値に「所有者」(owner)がいます。所有者はその値に対する責任を持ちます。
所有権の3つのルール
┌──────────────────────────────────────────────────┐
│ 所有権の3つのルール │
├──────────────────────────────────────────────────┤
│ 1. Rustの各値は「所有者」と呼ばれる変数を持つ │
│ 2. 所有者は同時に1つだけ存在できる │
│ 3. 所有者がスコープを抜けると、値は破棄される │
└──────────────────────────────────────────────────┘
この3つのルールがRustのメモリ安全性を支えています。
ルール1: 各値には所有者がいる
fn main() {
let s = String::from("hello"); // sが"hello"の所有者
println!("{}", s);
}
sが"hello"という文字列の所有者です。
ルール2: 所有者は同時に1つだけ
fn main() {
let s1 = String::from("hello");
let s2 = s1; // 所有権がs1からs2に移動(ムーブ)
// println!("{}", s1); // エラー!s1はもう所有者ではない
println!("{}", s2); // OK
}
ムーブの図解
s1 = String::from("hello");
スタック ヒープ
┌─────────────┐ ┌───────────┐
│ s1 │ │ │
│ ptr ─────────────────────→│ "hello" │
│ len: 5 │ │ │
│ cap: 5 │ └───────────┘
└─────────────┘
s2 = s1; // ムーブ後
┌─────────────┐ ┌───────────┐
│ s1 (無効) │ │ │
│ ptr ────X │ │ "hello" │
│ len: 5 │ │ │
│ cap: 5 │ └───────────┘
└─────────────┘ ↑
┌─────────────┐ │
│ s2 │ │
│ ptr ─────────────────────────────
│ len: 5 │
│ cap: 5 │
└─────────────┘
ルール3: スコープを抜けると値は破棄
fn main() {
{
let s = String::from("hello");
println!("{}", s);
} // ← ここでsがスコープを抜ける。メモリが解放される。
// println!("{}", s); // エラー!sは存在しない
}
なぜこれが重要なのか
C言語などでは、メモリの解放を忘れるとメモリリークが起きます。 逆に、同じメモリを2回解放すると二重解放バグが起きます。
Rustは所有権ルールにより、これらをコンパイル時に防ぎます。
コピーとムーブ
コピーされる型(Copy型)
スタックに収まる小さな型は、代入時にコピーされます。
fn main() {
let x = 5;
let y = x; // コピー
println!("x = {}, y = {}", x, y); // 両方使える
}
Copy型の例: i32, f64, bool, char, タプル(全要素がCopyの場合)
ムーブされる型
ヒープを使う型は、代入時にムーブ(所有権の移動)されます。
fn main() {
let s1 = String::from("hello");
let s2 = s1; // ムーブ
// println!("{}", s1); // エラー!
println!("{}", s2);
}
ムーブされる型の例: String, Vec<T>, Box<T>
clone: 明示的なコピー
ヒープのデータもコピーしたい場合は、cloneを使います。
fn main() {
let s1 = String::from("hello");
let s2 = s1.clone(); // ヒープのデータもコピー
println!("s1 = {}, s2 = {}", s1, s2); // 両方使える
}
clone後のメモリ:
スタック ヒープ
┌─────────────┐ ┌───────────┐
│ s1 │ │ "hello" │ ← s1用
│ ptr ─────────────────────→│ │
└─────────────┘ └───────────┘
┌─────────────┐ ┌───────────┐
│ s2 │ │ "hello" │ ← s2用(別の場所)
│ ptr ─────────────────────→│ │
└─────────────┘ └───────────┘
注意: cloneはヒープのデータをコピーするので、大きなデータでは遅くなります。
関数と所有権
関数に渡すとムーブする
fn main() {
let s = String::from("hello");
takes_ownership(s); // sの所有権が関数に移動
// println!("{}", s); // エラー!sは無効
}
fn takes_ownership(some_string: String) {
println!("{}", some_string);
} // some_stringがスコープを抜けてメモリ解放
関数から返すと所有権が移動
fn main() {
let s1 = gives_ownership(); // 所有権をもらう
println!("{}", s1);
let s2 = String::from("hello");
let s3 = takes_and_gives_back(s2); // s2を渡して、新しい所有権をもらう
// println!("{}", s2); // エラー!
println!("{}", s3); // OK
}
fn gives_ownership() -> String {
String::from("hello")
}
fn takes_and_gives_back(s: String) -> String {
s
}
所有権のまとめ図
┌────────────────────────────────────────────────────────┐
│ 所有権の移動パターン │
├────────────────────────────────────────────────────────┤
│ │
│ 変数代入: let s2 = s1; → s1からs2にムーブ │
│ │
│ 関数呼出: func(s); → sから引数にムーブ │
│ │
│ 関数戻値: let s = func(); → 関数からsにムーブ │
│ │
│ clone: let s2 = s1.clone(); → コピー(両方使える) │
│ │
└────────────────────────────────────────────────────────┘
よくあるエラーと対処
エラー: use of moved value
fn main() {
let s = String::from("hello");
let s2 = s;
println!("{}", s); // error: use of moved value: `s`
}
対処法:
cloneを使う:let s2 = s.clone();- 参照を使う(次のドキュメントで学習):
let s2 = &s;
まとめ
| ルール | 内容 |
|---|---|
| ルール1 | 各値には所有者がいる |
| ルール2 | 所有者は同時に1つだけ |
| ルール3 | スコープを抜けると値は破棄 |
| 操作 | Copy型 | ムーブ型 |
|---|---|---|
| 代入 | コピー | ムーブ |
| 関数に渡す | コピー | ムーブ |
| 両方使いたい | そのまま | clone |
確認テスト
Q1. 所有権のルールとして正しいものはどれですか?
Q2. 以下のうち、Copy型(代入時にコピーされる型)はどれですか?
i32などの整数型、f64などの浮動小数点型、bool、charはCopy型で、代入時にコピーされます。String、Vec、Boxはヒープを使うため、ムーブされます。
Q3. 以下のコードはコンパイルできますか?let s1 = String::from("hello"); let s2 = s1; println!("{}", s1);
let s2 = s1;で所有権がs1からs2にムーブしたため、s1は無効になっています。無効な変数を使おうとしているのでエラーになります。エラーメッセージ: error[E0382]: borrow of moved value: 's1'
Q4. 関数にStringを渡した後も元の変数を使いたい場合、最も効率的な方法はどれですか?
clone()はヒープデータをコピーするためコストがかかり、所有権を返す方法は関数のシグネチャが複雑になります。参照を使えばコピーなしでデータにアクセスでき、所有権も保持できます。
Q5. clone()メソッドについて正しい説明はどれですか?
clone()はヒープ上のデータも含めて深くコピーします。これにより、元の変数と新しい変数の両方が独立したデータを持ちます。ただし、大きなデータでは処理コストがかかります。
次のドキュメント: 03_references_borrowing.md
参照と借用
所有権を移動させずにデータを使う「参照」と「借用」について学びます。
問題: 所有権を移動させたくない
前回の例を思い出してください:
fn main() {
let s = String::from("hello");
let len = calculate_length(s); // 所有権がムーブ
// println!("{}", s); // エラー!
}
fn calculate_length(s: String) -> usize {
s.len()
}
値を使いたいだけなのに、所有権を渡さなければいけないのは不便です。
解決策: 参照(Reference)
参照を使うと、所有権を移動させずにデータにアクセスできます。
fn main() {
let s = String::from("hello");
let len = calculate_length(&s); // 参照を渡す
println!("'{}'の長さは{}", s, len); // sはまだ使える!
}
fn calculate_length(s: &String) -> usize {
s.len()
}
参照の図解
let s = String::from("hello");
let r = &s;
スタック ヒープ
┌─────────────┐
│ s │ ┌───────────┐
│ ptr ─────────────────────→│ "hello" │
│ len: 5 │ └───────────┘
│ cap: 5 │ ↑
└─────────────┘ │
↑ │
┌──────┴──────┐ │
│ r (&s) │ │
│ ptr ────────┘ (sを指す)
└─────────────┘
rはsを参照しています。sの所有権は移動しません。
借用(Borrowing)
参照を作ることを借用(Borrowing)と呼びます。
本を図書館から借りるイメージです:
- 図書館(所有者)は本を持っている
- あなた(借用者)は本を読める
- 読み終わったら返す(参照のスコープが終わる)
- 本は図書館のまま(所有権は移動しない)
不変参照(Immutable Reference)
&で作る参照は読み取り専用です。
fn main() {
let s = String::from("hello");
let r1 = &s;
let r2 = &s; // 複数の不変参照OK
println!("{}, {}", r1, r2);
}
不変参照のルール
- 複数の不変参照を同時に持てる
- 参照先のデータを変更できない
fn main() {
let s = String::from("hello");
let r = &s;
// r.push_str(" world"); // エラー!不変参照からは変更できない
}
可変参照(Mutable Reference)
&mutで作る参照は変更可能です。
fn main() {
let mut s = String::from("hello");
change(&mut s);
println!("{}", s); // "hello, world"
}
fn change(s: &mut String) {
s.push_str(", world");
}
可変参照のルール(重要!)
ルール: 可変参照は同時に1つだけ
fn main() {
let mut s = String::from("hello");
let r1 = &mut s;
// let r2 = &mut s; // エラー!可変参照は1つだけ
println!("{}", r1);
}
なぜ1つだけ?
データ競合を防ぐためです。
データ競合は以下の3つが同時に起きると発生します:
- 2つ以上のポインタが同じデータにアクセス
- 少なくとも1つが書き込みを行う
- 同期機構がない
Rustは「可変参照は1つだけ」というルールで、コンパイル時にこれを防ぎます。
不変参照と可変参照の混在
不変参照がある間は、可変参照を作れません
fn main() {
let mut s = String::from("hello");
let r1 = &s; // OK
let r2 = &s; // OK
// let r3 = &mut s; // エラー!不変参照があるので可変参照は作れない
println!("{}, {}", r1, r2);
}
ただし、不変参照が使われなくなった後なら可変参照を作れます:
fn main() {
let mut s = String::from("hello");
let r1 = &s;
let r2 = &s;
println!("{}, {}", r1, r2); // r1, r2はここまで
let r3 = &mut s; // OK!r1, r2はもう使われない
println!("{}", r3);
}
参照のルールまとめ
┌────────────────────────────────────────────────────┐
│ 参照のルール │
├────────────────────────────────────────────────────┤
│ 1. 不変参照(&T)は複数同時に存在できる │
│ 2. 可変参照(&mut T)は同時に1つだけ │
│ 3. 不変参照と可変参照は同時に存在できない │
│ 4. 参照は常に有効でなければならない │
└────────────────────────────────────────────────────┘
ダングリング参照
ダングリング参照とは、解放されたメモリを指す参照です。
fn main() {
let r = dangle();
}
fn dangle() -> &String { // コンパイルエラー!
let s = String::from("hello");
&s // sへの参照を返そうとしている
} // ← sがドロップされる。参照先がなくなる!
Rustはこれをコンパイル時に防ぎます。
解決策: 所有権を返す
#![allow(unused)]
fn main() {
fn no_dangle() -> String {
let s = String::from("hello");
s // 所有権を返す(参照ではなく値自体)
}
}
実践例
例1: 最長の文字列を返す
fn main() {
let s1 = String::from("hello");
let s2 = String::from("hi");
let result = longest(&s1, &s2);
println!("最長: {}", result);
}
fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
if x.len() > y.len() {
x
} else {
y
}
}
('aはライフタイムで、次のドキュメントで学びます)
例2: Vecの要素を変更
fn main() {
let mut numbers = vec![1, 2, 3, 4, 5];
for n in &mut numbers {
*n *= 2; // 各要素を2倍に
}
println!("{:?}", numbers); // [2, 4, 6, 8, 10]
}
まとめ
| 種類 | 構文 | 特徴 |
|---|---|---|
| 不変参照 | &T | 読み取り専用、複数OK |
| 可変参照 | &mut T | 変更可能、1つだけ |
| ルール | 内容 |
|---|---|
| ルール1 | 不変参照は複数OK |
| ルール2 | 可変参照は1つだけ |
| ルール3 | 不変と可変は同時に存在不可 |
| ルール4 | 参照は常に有効(ダングリング禁止) |
確認テスト
Q1. 不変参照(&T)について正しいものはどれですか?
Q2. 可変参照(&mut T)について正しいものはどれですか?
Q3. 以下のコードはコンパイルできますか?let mut s = String::from("hello"); let r1 = &s; let r2 = &mut s; println!("{}, {}", r1, r2);
r1がまだ使用される前に、可変参照r2を作ろうとしています。不変参照と可変参照は同時に存在できません。r1を先に使い切れば、その後で可変参照を作ることができます。
Q4. 関数内で文字列を変更したい場合、引数の型として正しいものはどれですか?
&mut Stringが必要です。不変参照&Stringでは変更できず、所有権を受け取ると呼び出し元で使えなくなります。&strは読み取り専用のスライスです。
Q5. 「借用」(Borrowing)の概念について正しい説明はどれですか?
次のドキュメント: 04_lifetimes_basic.md
ライフタイム入門
参照が有効な期間を示す「ライフタイム」について学びます。
ライフタイムとは
ライフタイムは「参照が有効な期間」を表します。
fn main() {
let r; // ---------+-- 'a
// |
{ // |
let x = 5; // -+-- 'b |
r = &x; // | |
} // -+ | (xが破棄される)
// |
// println!("{}", r); // エラー! | (rは無効なメモリを指す)
} // ---------+
xのライフタイム'bはrのライフタイム'aより短いため、エラーになります。
なぜライフタイムが必要なのか
ダングリング参照を防ぐ
#![allow(unused)]
fn main() {
// これはコンパイルエラー
fn dangle() -> &String {
let s = String::from("hello");
&s // sへの参照を返そうとしている
} // ← sがドロップされる!参照先がなくなる
}
ライフタイムによって、コンパイラは「参照が参照先より長生きしないか」をチェックします。
関数のライフタイム注釈
問題のあるコード
#![allow(unused)]
fn main() {
// これはコンパイルエラー
fn longest(x: &str, y: &str) -> &str {
if x.len() > y.len() {
x
} else {
y
}
}
}
エラー:
error[E0106]: missing lifetime specifier
コンパイラは「返される参照がxのライフタイムなのかyのライフタイムなのかわからない」と言っています。
ライフタイム注釈で解決
#![allow(unused)]
fn main() {
fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
if x.len() > y.len() {
x
} else {
y
}
}
}
<'a>: ライフタイムパラメータの宣言&'a str: ライフタイム'aを持つ文字列参照
この注釈は「返される参照は、xとyの両方が有効な間だけ有効」という意味です。
ライフタイム注釈の構文
#![allow(unused)]
fn main() {
&i32 // 参照
&'a i32 // ライフタイム'aを持つ参照
&'a mut i32 // ライフタイム'aを持つ可変参照
}
具体例で理解する
fn main() {
let string1 = String::from("long string is long");
{
let string2 = String::from("xyz");
let result = longest(&string1, &string2);
println!("最長: {}", result); // OK: string2はまだ有効
}
// ここでresultを使おうとすると...?
}
fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
if x.len() > y.len() {
x
} else {
y
}
}
これはOKです。resultは内側のスコープ内でのみ使われており、その時点ではstring1もstring2も有効だからです。
エラーになるケース
fn main() {
let string1 = String::from("long string is long");
let result;
{
let string2 = String::from("xyz");
result = longest(&string1, &string2);
} // string2がドロップされる
println!("最長: {}", result); // エラー!string2はもうない
}
resultはstring2を参照する可能性があるのに、string2はスコープを抜けて無効になっています。
ライフタイム省略規則
すべてのケースでライフタイムを書く必要はありません。
省略できるケース
#![allow(unused)]
fn main() {
// 明示的なライフタイム
fn first_word<'a>(s: &'a str) -> &'a str { ... }
// 省略可能(コンパイラが推論)
fn first_word(s: &str) -> &str { ... }
}
省略規則(参考)
- 各引数の参照は別々のライフタイムを得る
- 入力ライフタイムが1つなら、それが出力に適用される
&selfや&mut selfがある場合、selfのライフタイムが出力に適用される
最初は「省略できることがある」程度の理解で大丈夫です。
構造体のライフタイム
参照を持つ構造体にはライフタイム注釈が必要です。
struct ImportantExcerpt<'a> {
part: &'a str,
}
fn main() {
let novel = String::from("Call me Ishmael. Some years ago...");
let first_sentence = novel.split('.').next().unwrap();
let excerpt = ImportantExcerpt {
part: first_sentence,
};
println!("{}", excerpt.part);
}
この構造体は「partが参照しているデータが有効な間だけ有効」です。
’static ライフタイム
'staticは「プログラム全体で有効」な特別なライフタイムです。
#![allow(unused)]
fn main() {
let s: &'static str = "Hello, world!";
}
文字列リテラルは'staticライフタイムを持ちます。プログラムに埋め込まれているため、常に有効です。
注意
'staticを安易に使うのは避けましょう。本当に必要なケースは稀です。
ライフタイムのメンタルモデル
┌────────────────────────────────────────────────────────┐
│ ライフタイム = 「参照が有効な期間の制約」 │
├────────────────────────────────────────────────────────┤
│ │
│ 目的: ダングリング参照を防ぐ │
│ │
│ ルール: │
│ ・参照は参照先より長生きしてはいけない │
│ ・関数が複数の参照を受け取り参照を返す場合、 │
│ ライフタイム注釈で関係を明示 │
│ │
│ 省略: 多くの場合、コンパイラが推論してくれる │
│ │
└────────────────────────────────────────────────────────┘
まとめ
| 概念 | 説明 |
|---|---|
| ライフタイム | 参照が有効な期間 |
'a | ライフタイムパラメータ |
| 注釈の目的 | 参照間の関係を明示 |
'static | プログラム全体で有効 |
最初は難しく感じますが、使っていくうちに自然と理解できるようになります。
確認テスト
Q1. ライフタイムの主な目的は何ですか?
Q2. 'staticライフタイムが意味するものは何ですか?
'staticはプログラムの開始から終了まで有効な最も長いライフタイムです。文字列リテラルは'staticライフタイムを持ちます。
Q3. 以下のコードはコンパイルできますか?let r; { let x = 5; r = &x; } println!("{}", r);
xは内側のスコープを抜けると破棄されますが、rは外側のスコープでxへの参照を使おうとしています。xのライフタイムがrより短いため、ダングリング参照になります。
Q4. 関数fn longest(x: &str, y: &str) -> &strがコンパイルエラーになる理由は何ですか?
xのライフタイムなのかyのライフタイムなのかわかりません。fn longest<'a>(x: &'a str, y: &'a str) -> &'a strのようにライフタイムパラメータを追加する必要があります。
Q5. 構造体に参照を持たせる場合、必要なものは何ですか?
struct Excerpt<'a> { text: &'a str }のようにライフタイムパラメータが必要です。これは「この構造体は参照先が有効な間だけ有効」ということを意味します。
次のドキュメント: 05_slices.md
スライス
コレクションの一部を参照する「スライス」について学びます。
スライスとは
スライスはコレクションの一部への参照です。所有権を持ちません。
fn main() {
let s = String::from("hello world");
let hello = &s[0..5]; // "hello"へのスライス
let world = &s[6..11]; // "world"へのスライス
println!("{}, {}", hello, world);
}
文字列スライス(&str)
基本的な使い方
fn main() {
let s = String::from("hello world");
let slice1 = &s[0..5]; // "hello"
let slice2 = &s[6..11]; // "world"
let slice3 = &s[0..]; // "hello world"(最初から)
let slice4 = &s[..5]; // "hello"(最初から5まで)
let slice5 = &s[..]; // "hello world"(全体)
println!("{}", slice1);
}
範囲の構文
| 構文 | 意味 | 例(“hello“の場合) |
|---|---|---|
[0..5] | 0から5未満 | “hello” |
[..5] | 最初から5未満 | “hello” |
[3..] | 3から最後まで | “lo” |
[..] | 全体 | “hello” |
[0..=4] | 0から4まで(含む) | “hello” |
メモリレイアウト
#![allow(unused)]
fn main() {
let s = String::from("hello world");
let hello = &s[0..5];
String (s) ヒープ
┌─────────────┐ ┌───────────────────────┐
│ ptr ─────────────────────→│ h e l l o w o r l d │
│ len: 11 │ └───────────────────────┘
│ cap: 11 │ ↑
└─────────────┘ │ ptr
┌──┴────────┐
&str (hello) │ len: 5 │
└───────────┘
}
&strは以下を持ちます:
- ポインタ(データの開始位置)
- 長さ
String と &str の関係
#![allow(unused)]
fn main() {
let s: String = String::from("hello"); // 所有権あり
let slice: &str = &s; // Stringへのスライス
let literal: &str = "hello"; // 文字列リテラル('static)
}
使い分け
| 型 | 特徴 | 用途 |
|---|---|---|
String | 所有権あり、変更可能 | 文字列を所有・変更したいとき |
&str | 参照、読み取り専用 | 文字列を読むだけのとき |
関数の引数は&strを推奨
#![allow(unused)]
fn main() {
// これより...
fn greet(name: &String) { ... }
// こちらが良い
fn greet(name: &str) { ... }
}
&strを受け取る関数は、Stringからも文字列リテラルからも呼び出せます:
fn greet(name: &str) {
println!("Hello, {}!", name);
}
fn main() {
let s = String::from("World");
greet(&s); // Stringから(自動的に&strに変換)
greet("World"); // 文字列リテラルから
}
配列スライス
配列やVecの一部も参照できます。
fn main() {
let arr = [1, 2, 3, 4, 5];
let slice: &[i32] = &arr[1..4]; // [2, 3, 4]
println!("{:?}", slice);
}
Vecのスライス
fn main() {
let v = vec![1, 2, 3, 4, 5];
let slice = &v[1..4]; // &[i32]型
println!("{:?}", slice); // [2, 3, 4]
}
スライスを使った関数
最初の単語を返す
fn first_word(s: &str) -> &str {
let bytes = s.as_bytes();
for (i, &byte) in bytes.iter().enumerate() {
if byte == b' ' {
return &s[0..i];
}
}
&s[..]
}
fn main() {
let s = String::from("hello world");
let word = first_word(&s);
println!("最初の単語: {}", word); // "hello"
}
配列の合計を計算
fn sum(numbers: &[i32]) -> i32 {
let mut total = 0;
for n in numbers {
total += n;
}
total
}
fn main() {
let arr = [1, 2, 3, 4, 5];
let vec = vec![10, 20, 30];
println!("配列の合計: {}", sum(&arr)); // 15
println!("Vecの合計: {}", sum(&vec)); // 60
println!("一部の合計: {}", sum(&arr[1..4])); // 9 (2+3+4)
}
スライスの安全性
スライスは参照なので、所有権ルールに従います。
fn main() {
let mut s = String::from("hello world");
let word = first_word(&s); // 不変借用
// s.clear(); // エラー!wordが有効な間は変更できない
println!("{}", word);
}
fn first_word(s: &str) -> &str {
&s[..5]
}
よくあるパターン
パターン1: イテレーションしながらインデックスを使わない
#![allow(unused)]
fn main() {
// 良くない(インデックスアクセス)
fn process_bad(data: &[i32]) {
for i in 0..data.len() {
println!("{}", data[i]);
}
}
// 良い(イテレータ使用)
fn process_good(data: &[i32]) {
for item in data {
println!("{}", item);
}
}
}
パターン2: 範囲チェック
#![allow(unused)]
fn main() {
fn safe_get(data: &[i32], index: usize) -> Option<&i32> {
data.get(index) // 範囲外ならNone
}
}
まとめ
| スライス型 | 元のデータ | 用途 |
|---|---|---|
&str | String, 文字列リテラル | 文字列の一部を参照 |
&[T] | 配列, Vec<T> | コレクションの一部を参照 |
| 特徴 | 内容 |
|---|---|
| 所有権 | なし(参照) |
| サイズ | ポインタ + 長さ |
| 安全性 | 借用ルールに従う |
確認テスト
Q1. &strとStringの違いは?
Stringは文字列データの所有権を持ち、変更可能です。&strは文字列への参照(スライス)で、所有権を持たず、読み取り専用です。
Q2. 関数の引数として文字列を受け取る場合、推奨される型は?
&strを受け取る関数は、Stringからも文字列リテラルからも呼び出せるため、より柔軟です。&StringだとStringからしか呼び出せません。
Q3. 以下のコードの出力は?let s = String::from("hello world"); let part1 = &s[..5]; let part2 = &s[6..]; println!("{}-{}", part1, part2);
&s[..5]はインデックス0から5未満で「hello」、&s[6..]はインデックス6から最後までで「world」になります。
Q4. let mut s = String::from("hello"); let slice = &s[..]; s.push_str(" world");がエラーになる理由は?
sliceがsへの不変参照を持っている間に、s.push_str()でsを変更しようとしているためエラーになります。スライスを先に使い切るか、変更後にスライスを取得する必要があります。
Q5. スライス&[T]の特徴として正しいものは?
&[T]は配列やVec<T>の一部(または全体)を参照できます。所有権は持たず、借用ルールに従います。
Phase 2 完了!
おめでとうございます!Rust最重要のPhase 2を完了しました。
学んだこと:
- スタックとヒープの違い
- 所有権の3つのルール
- 参照と借用(不変参照、可変参照)
- ライフタイムの基礎
- スライス(&str、&[T])
**これらの概念はRustの核心です。**最初は難しく感じても、コードを書いていくうちに自然と理解できるようになります。
次のPhase: Phase 3: 構造化プログラミング
構造体(Struct)
関連するデータをまとめる「構造体」について学びます。
構造体とは
構造体は複数の関連するデータをまとめた型です。
#![allow(unused)]
fn main() {
struct User {
username: String,
email: String,
age: u32,
active: bool,
}
}
構造体の定義と使用
定義
#![allow(unused)]
fn main() {
struct Point {
x: f64,
y: f64,
}
}
インスタンスの作成
fn main() {
let p = Point {
x: 3.0,
y: 4.0,
};
println!("座標: ({}, {})", p.x, p.y);
}
フィールドへのアクセス
fn main() {
let mut user = User {
username: String::from("太郎"),
email: String::from("taro@example.com"),
age: 25,
active: true,
};
println!("名前: {}", user.username);
// mutなら変更可能
user.age = 26;
}
フィールド初期化省略記法
変数名とフィールド名が同じなら省略できます。
#![allow(unused)]
fn main() {
fn create_user(username: String, email: String) -> User {
User {
username, // username: username の省略
email, // email: email の省略
age: 0,
active: true,
}
}
}
構造体更新構文
既存のインスタンスから一部だけ変えた新しいインスタンスを作れます。
fn main() {
let user1 = User {
username: String::from("太郎"),
email: String::from("taro@example.com"),
age: 25,
active: true,
};
let user2 = User {
email: String::from("jiro@example.com"),
..user1 // 残りはuser1から
};
// 注意: user1.usernameはムーブされたので使えない
// println!("{}", user1.username); // エラー!
println!("{}", user1.age); // OK(Copyトレイト)
}
タプル構造体
フィールド名のない構造体です。
struct Color(u8, u8, u8);
struct Point3D(f64, f64, f64);
fn main() {
let black = Color(0, 0, 0);
let origin = Point3D(0.0, 0.0, 0.0);
println!("R: {}", black.0);
println!("x: {}", origin.0);
}
ユニット様構造体
フィールドを持たない構造体です。トレイト実装に使います。
struct AlwaysEqual;
fn main() {
let _subject = AlwaysEqual;
}
メソッドの定義(impl)
構造体に関連する関数をimplブロックで定義します。
struct Rectangle {
width: u32,
height: u32,
}
impl Rectangle {
// メソッド(&selfを受け取る)
fn area(&self) -> u32 {
self.width * self.height
}
// 可変メソッド(&mut selfを受け取る)
fn double(&mut self) {
self.width *= 2;
self.height *= 2;
}
// 関連関数(selfを受け取らない)
fn square(size: u32) -> Rectangle {
Rectangle {
width: size,
height: size,
}
}
}
fn main() {
let mut rect = Rectangle {
width: 30,
height: 50,
};
println!("面積: {}", rect.area());
rect.double();
println!("2倍後の面積: {}", rect.area());
let sq = Rectangle::square(10);
println!("正方形の面積: {}", sq.area());
}
&self、&mut self、self の違い
| 引数 | 意味 | 用途 |
|---|---|---|
&self | 不変借用 | 読み取りのみ |
&mut self | 可変借用 | 値を変更 |
self | 所有権を取得 | インスタンスを消費 |
複数のimplブロック
#![allow(unused)]
fn main() {
impl Rectangle {
fn area(&self) -> u32 {
self.width * self.height
}
}
impl Rectangle {
fn can_hold(&self, other: &Rectangle) -> bool {
self.width > other.width && self.height > other.height
}
}
}
デバッグ出力
#[derive(Debug)]を使うと、構造体を簡単に表示できます。
#[derive(Debug)]
struct Rectangle {
width: u32,
height: u32,
}
fn main() {
let rect = Rectangle {
width: 30,
height: 50,
};
println!("{:?}", rect); // Rectangle { width: 30, height: 50 }
println!("{:#?}", rect); // 整形表示
}
実践例: ユーザー管理
#[derive(Debug)]
struct User {
id: u32,
username: String,
email: String,
}
impl User {
fn new(id: u32, username: String, email: String) -> User {
User { id, username, email }
}
fn display(&self) {
println!("ID: {}, 名前: {}", self.id, self.username);
}
fn update_email(&mut self, new_email: String) {
self.email = new_email;
}
}
fn main() {
let mut user = User::new(1, String::from("太郎"), String::from("taro@example.com"));
user.display();
user.update_email(String::from("newtaro@example.com"));
println!("新しいメール: {}", user.email);
}
まとめ
| 概念 | 説明 |
|---|---|
| 構造体 | 関連データをまとめる |
impl | メソッドを定義 |
&self | 読み取りメソッド |
&mut self | 変更メソッド |
| 関連関数 | Self::func()で呼ぶ |
#[derive(Debug)] | デバッグ出力を有効化 |
確認テスト
Q1. メソッド定義で&selfを使う場合、何ができる?
&selfは不変借用なので、読み取りのみ可能です。変更するには&mut selfを使います。
Q2. Rectangle::square(10)のような呼び出しは何と呼ばれる?
selfを引数に取らない関数は関連関数と呼ばれ、Type::function()の形式で呼び出します。newのようなファクトリ関数によく使われます。
Q3. 以下のコードの出力は?impl Counter { fn increment(&mut self) { self.value += 1; } } let mut c = Counter { value: 0 }; c.increment(); c.increment(); println!("{}", c.value);
incrementメソッドを2回呼び出すので、valueは0→1→2となります。
Q4. 以下のコードがコンパイルエラーになる理由は?impl Point { fn move_by(&self, dx: f64, dy: f64) { self.x += dx; self.y += dy; } }
&self(不変借用)で値を変更しようとしているためエラーになります。値を変更するには&mut selfを使います。
Q5. 構造体に#[derive(Debug)]を付けると何ができる?
#[derive(Debug)]を使うと、println!("{:?}", value)やprintln!("{:#?}", value)で構造体の内容を表示できるようになります。
次のドキュメント: 02_enums_pattern.md
列挙型とパターンマッチング
複数の選択肢を表す「列挙型」と、それを扱う「パターンマッチング」を学びます。
列挙型(Enum)とは
列挙型は複数の選択肢のうち1つを表す型です。
enum Direction {
North,
South,
East,
West,
}
fn main() {
let dir = Direction::North;
}
列挙型の定義と使用
基本的な列挙型
enum Status {
Pending,
InProgress,
Completed,
Cancelled,
}
fn main() {
let task_status = Status::InProgress;
}
データを持つ列挙型
各バリアントがデータを持てます。
enum Message {
Quit, // データなし
Move { x: i32, y: i32 }, // 構造体風
Write(String), // 単一の値
ChangeColor(u8, u8, u8), // タプル風
}
fn main() {
let msg1 = Message::Quit;
let msg2 = Message::Move { x: 10, y: 20 };
let msg3 = Message::Write(String::from("Hello"));
let msg4 = Message::ChangeColor(255, 0, 0);
}
match式
matchは列挙型のすべてのバリアントを処理します。
#![allow(unused)]
fn main() {
enum Coin {
Penny,
Nickel,
Dime,
Quarter,
}
fn value_in_cents(coin: Coin) -> u32 {
match coin {
Coin::Penny => 1,
Coin::Nickel => 5,
Coin::Dime => 10,
Coin::Quarter => 25,
}
}
}
matchは網羅的
すべてのバリアントを扱わないとエラーになります。
#![allow(unused)]
fn main() {
fn value_in_cents(coin: Coin) -> u32 {
match coin {
Coin::Penny => 1,
// エラー!他のバリアントが抜けている
}
}
}
データの取り出し
#![allow(unused)]
fn main() {
enum Message {
Quit,
Move { x: i32, y: i32 },
Write(String),
}
fn process(msg: Message) {
match msg {
Message::Quit => {
println!("終了します");
}
Message::Move { x, y } => {
println!("移動: ({}, {})", x, y);
}
Message::Write(text) => {
println!("メッセージ: {}", text);
}
}
}
}
_ プレースホルダ
残りすべてにマッチします。
#![allow(unused)]
fn main() {
fn describe_number(n: i32) {
match n {
1 => println!("一"),
2 => println!("二"),
3 => println!("三"),
_ => println!("その他"), // 1, 2, 3以外すべて
}
}
}
Option型(復習と深掘り)
Optionは「値があるかないか」を表す標準の列挙型です。
#![allow(unused)]
fn main() {
enum Option<T> {
Some(T),
None,
}
}
Optionの使用
fn find_user(id: u32) -> Option<String> {
if id == 1 {
Some(String::from("太郎"))
} else {
None
}
}
fn main() {
match find_user(1) {
Some(name) => println!("見つかりました: {}", name),
None => println!("見つかりませんでした"),
}
}
Result型(復習と深掘り)
Resultは「成功か失敗か」を表す標準の列挙型です。
#![allow(unused)]
fn main() {
enum Result<T, E> {
Ok(T),
Err(E),
}
}
Resultの使用
fn divide(a: f64, b: f64) -> Result<f64, String> {
if b == 0.0 {
Err(String::from("ゼロ除算エラー"))
} else {
Ok(a / b)
}
}
fn main() {
match divide(10.0, 2.0) {
Ok(result) => println!("結果: {}", result),
Err(e) => println!("エラー: {}", e),
}
}
if let 式
特定のパターンだけ処理したい場合に便利です。
fn main() {
let some_value = Some(5);
// matchを使う場合
match some_value {
Some(n) => println!("値: {}", n),
None => (), // 何もしない
}
// if letを使う場合(簡潔)
if let Some(n) = some_value {
println!("値: {}", n);
}
}
if let else
fn main() {
let coin = Coin::Quarter;
if let Coin::Quarter = coin {
println!("25セント!");
} else {
println!("25セントではない");
}
}
while let 式
パターンがマッチする間ループします。
fn main() {
let mut stack = vec![1, 2, 3];
while let Some(top) = stack.pop() {
println!("{}", top);
}
}
// 出力: 3, 2, 1
列挙型のメソッド
列挙型にもメソッドを定義できます。
enum Status {
Pending,
InProgress,
Completed,
}
impl Status {
fn description(&self) -> &str {
match self {
Status::Pending => "未着手",
Status::InProgress => "進行中",
Status::Completed => "完了",
}
}
fn is_done(&self) -> bool {
matches!(self, Status::Completed)
}
}
fn main() {
let status = Status::InProgress;
println!("{}", status.description());
println!("完了?: {}", status.is_done());
}
実践例: 状態管理
#![allow(unused)]
fn main() {
#[derive(Debug)]
enum TaskState {
Todo,
InProgress { progress: u8 },
Done { completed_at: String },
}
struct Task {
name: String,
state: TaskState,
}
impl Task {
fn new(name: String) -> Task {
Task {
name,
state: TaskState::Todo,
}
}
fn start(&mut self) {
self.state = TaskState::InProgress { progress: 0 };
}
fn update_progress(&mut self, progress: u8) {
if let TaskState::InProgress { progress: ref mut p } = self.state {
*p = progress;
}
}
fn complete(&mut self, completed_at: String) {
self.state = TaskState::Done { completed_at };
}
}
}
まとめ
| 概念 | 説明 |
|---|---|
| 列挙型 | 複数の選択肢のうち1つを表す |
| バリアント | 列挙型の各選択肢 |
match | 網羅的なパターンマッチング |
if let | 特定パターンのみ処理 |
Option | 値の有無を表す |
Result | 成功/失敗を表す |
確認テスト
Q1. match式の特徴として正しいのは?
matchは網羅的で、すべてのケースを処理しないとコンパイルエラーになります。_を使えば残りすべてをまとめて処理できます。
Q2. if letを使うべき場面は?
if letは特定のパターンのみ処理し、残りは無視(またはelseで処理)したい場合に便利です。
Q3. 以下のコードの出力は?let light = Light::Yellow; let action = match light { Light::Red => "止まれ", Light::Yellow => "注意", Light::Green => "進め" }; println!("{}", action);
lightはLight::Yellowなので、matchで"注意"が返されます。
Q4. 以下のコードがコンパイルエラーになる理由は?enum Color { Red, Green, Blue } fn describe(c: Color) -> &str { match c { Color::Red => "赤", Color::Green => "緑" } }
matchは網羅的である必要があり、Color::Blueのケースが抜けているためコンパイルエラーになります。
Q5. Option<T>のNoneは何を表す?
Option<T>のNoneは「値が存在しない」ことを表します。Some(T)は「値が存在する」ことを表します。
次のドキュメント: 03_traits.md
トレイト(Trait)
共通の振る舞いを定義する「トレイト」について学びます。
トレイトとは
トレイトは型が持つべき振る舞い(メソッド)を定義します。他の言語の「インターフェース」に似ています。
#![allow(unused)]
fn main() {
trait Summary {
fn summarize(&self) -> String;
}
}
トレイトの定義と実装
トレイトを定義
#![allow(unused)]
fn main() {
trait Greet {
fn greet(&self) -> String;
}
}
型にトレイトを実装
struct Person {
name: String,
}
impl Greet for Person {
fn greet(&self) -> String {
format!("こんにちは、{}です!", self.name)
}
}
struct Robot {
id: u32,
}
impl Greet for Robot {
fn greet(&self) -> String {
format!("ピポパポ、ロボット{}号です", self.id)
}
}
fn main() {
let person = Person { name: String::from("太郎") };
let robot = Robot { id: 42 };
println!("{}", person.greet());
println!("{}", robot.greet());
}
デフォルト実装
トレイトにデフォルトの実装を提供できます。
#![allow(unused)]
fn main() {
trait Summary {
fn summarize(&self) -> String {
String::from("(詳細なし)")
}
}
struct Article {
title: String,
content: String,
}
// デフォルト実装をそのまま使う
impl Summary for Article {}
struct Tweet {
username: String,
text: String,
}
// カスタム実装で上書き
impl Summary for Tweet {
fn summarize(&self) -> String {
format!("@{}: {}", self.username, self.text)
}
}
}
トレイト境界(引数で使う)
「このトレイトを実装した型」を引数に取れます。
#![allow(unused)]
fn main() {
// impl Trait 構文(簡潔)
fn notify(item: &impl Summary) {
println!("速報!{}", item.summarize());
}
// トレイト境界構文(より明示的)
fn notify2<T: Summary>(item: &T) {
println!("速報!{}", item.summarize());
}
}
複数のトレイト境界
#![allow(unused)]
fn main() {
fn notify(item: &(impl Summary + Display)) {
// SummaryとDisplayの両方を実装した型のみ
}
// または
fn notify<T: Summary + Display>(item: &T) {
// ...
}
}
where句(複雑な場合)
#![allow(unused)]
fn main() {
fn some_function<T, U>(t: &T, u: &U)
where
T: Summary + Clone,
U: Clone + Debug,
{
// ...
}
}
トレイトを返す
#![allow(unused)]
fn main() {
fn create_summarizable() -> impl Summary {
Tweet {
username: String::from("user"),
text: String::from("Hello!"),
}
}
}
標準ライブラリの重要なトレイト
Debug - デバッグ出力
#[derive(Debug)]
struct Point {
x: i32,
y: i32,
}
fn main() {
let p = Point { x: 1, y: 2 };
println!("{:?}", p);
}
Clone - 明示的なコピー
#[derive(Clone)]
struct Data {
value: String,
}
fn main() {
let d1 = Data { value: String::from("hello") };
let d2 = d1.clone();
}
PartialEq - 等価比較
#[derive(PartialEq)]
struct Point {
x: i32,
y: i32,
}
fn main() {
let p1 = Point { x: 1, y: 2 };
let p2 = Point { x: 1, y: 2 };
println!("{}", p1 == p2); // true
}
Display - ユーザー向け表示
use std::fmt;
struct Point {
x: i32,
y: i32,
}
impl fmt::Display for Point {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "({}, {})", self.x, self.y)
}
}
fn main() {
let p = Point { x: 1, y: 2 };
println!("{}", p); // (1, 2)
}
Default - デフォルト値
#[derive(Default)]
struct Config {
debug: bool,
max_connections: u32,
}
fn main() {
let config = Config::default();
// debug: false, max_connections: 0
}
derive マクロ
よく使うトレイトはderiveで自動実装できます。
#![allow(unused)]
fn main() {
#[derive(Debug, Clone, PartialEq, Default)]
struct User {
name: String,
age: u32,
}
}
実践例: 動物の鳴き声
trait Animal {
fn name(&self) -> &str;
fn speak(&self) -> String;
fn introduce(&self) -> String {
format!("私は{}です。{}", self.name(), self.speak())
}
}
struct Dog {
name: String,
}
impl Animal for Dog {
fn name(&self) -> &str {
&self.name
}
fn speak(&self) -> String {
String::from("ワンワン!")
}
}
struct Cat {
name: String,
}
impl Animal for Cat {
fn name(&self) -> &str {
&self.name
}
fn speak(&self) -> String {
String::from("ニャー")
}
}
fn main() {
let dog = Dog { name: String::from("ポチ") };
let cat = Cat { name: String::from("タマ") };
println!("{}", dog.introduce());
println!("{}", cat.introduce());
}
まとめ
| 概念 | 説明 |
|---|---|
| トレイト | 共通の振る舞いを定義 |
impl Trait for Type | 型にトレイトを実装 |
| デフォルト実装 | トレイト内でデフォルトのメソッドを提供 |
| トレイト境界 | 「このトレイトを実装した型」という制約 |
derive | 標準トレイトの自動実装 |
確認テスト
Q1. トレイトの役割は?
Q2. #[derive(Debug)]の効果は?
deriveマクロは指定したトレイトの実装を自動生成します。Debugを実装すると{:?}で出力できるようになります。
Q3. 以下のコードで、Silent.speak()とLoud.speak()の出力は?trait Speak { fn speak(&self) -> String { String::from("...") } } struct Silent; impl Speak for Silent {} struct Loud; impl Speak for Loud { fn speak(&self) -> String { String::from("HELLO!") } }
Silentはデフォルト実装を使い "..." を出力し、Loudはカスタム実装で上書きして "HELLO!" を出力します。
Q4. fn show(item: Printable)がエラーになる理由は?
&impl Printableや&dyn Printableのように使う必要があります。
Q5. トレイト境界T: Clone + Debugは何を意味する?
+は「かつ」を意味し、TはCloneとDebugの両方のトレイトを実装している型でなければなりません。
次のドキュメント: 04_generics.md
ジェネリクス
型をパラメータ化する「ジェネリクス」について学びます。
ジェネリクスとは
ジェネリクスは型を抽象化し、同じコードを複数の型で使えるようにします。
#![allow(unused)]
fn main() {
// i32用
fn largest_i32(list: &[i32]) -> i32 { ... }
// f64用
fn largest_f64(list: &[f64]) -> f64 { ... }
// ジェネリクスで1つに
fn largest<T>(list: &[T]) -> T { ... }
}
関数のジェネリクス
fn largest<T: PartialOrd + Copy>(list: &[T]) -> T {
let mut largest = list[0];
for &item in list {
if item > largest {
largest = item;
}
}
largest
}
fn main() {
let numbers = vec![34, 50, 25, 100, 65];
println!("最大: {}", largest(&numbers));
let chars = vec!['y', 'm', 'a', 'q'];
println!("最大: {}", largest(&chars));
}
型パラメータの慣習
T- Type(一般的な型)E- Error(エラー型)K,V- Key, Value(マップ用)R- Result(結果型)
構造体のジェネリクス
struct Point<T> {
x: T,
y: T,
}
fn main() {
let integer_point = Point { x: 5, y: 10 };
let float_point = Point { x: 1.0, y: 4.0 };
}
複数の型パラメータ
struct Point<T, U> {
x: T,
y: U,
}
fn main() {
let mixed = Point { x: 5, y: 4.0 };
}
ジェネリクス構造体のメソッド
#![allow(unused)]
fn main() {
struct Point<T> {
x: T,
y: T,
}
impl<T> Point<T> {
fn x(&self) -> &T {
&self.x
}
}
// 特定の型にのみ実装
impl Point<f64> {
fn distance_from_origin(&self) -> f64 {
(self.x.powi(2) + self.y.powi(2)).sqrt()
}
}
}
列挙型のジェネリクス
標準ライブラリのOptionとResultがまさにこれです。
#![allow(unused)]
fn main() {
enum Option<T> {
Some(T),
None,
}
enum Result<T, E> {
Ok(T),
Err(E),
}
}
自作のジェネリクス列挙型
enum Response<T> {
Success(T),
Loading,
Error(String),
}
fn main() {
let user_response: Response<String> = Response::Success(String::from("太郎"));
let count_response: Response<i32> = Response::Success(42);
}
トレイト境界
ジェネリクスに「このトレイトを実装している型のみ」という制約をつけます。
#![allow(unused)]
fn main() {
use std::fmt::Display;
fn print_info<T: Display>(item: T) {
println!("情報: {}", item);
}
}
複数のトレイト境界
#![allow(unused)]
fn main() {
fn compare_and_display<T: PartialOrd + Display>(a: T, b: T) {
if a > b {
println!("{} > {}", a, b);
} else {
println!("{} <= {}", a, b);
}
}
}
where句
境界が複雑な場合に読みやすくなります。
#![allow(unused)]
fn main() {
fn some_function<T, U>(t: T, u: U) -> i32
where
T: Display + Clone,
U: Clone + Debug,
{
// ...
}
}
実践例: ジェネリックなペア
#[derive(Debug)]
struct Pair<T> {
first: T,
second: T,
}
impl<T> Pair<T> {
fn new(first: T, second: T) -> Pair<T> {
Pair { first, second }
}
fn swap(&mut self) {
std::mem::swap(&mut self.first, &mut self.second);
}
}
impl<T: PartialOrd + Display> Pair<T> {
fn cmp_display(&self) {
if self.first > self.second {
println!("最大は {}", self.first);
} else {
println!("最大は {}", self.second);
}
}
}
fn main() {
let mut pair = Pair::new(5, 10);
pair.cmp_display();
pair.swap();
println!("{:?}", pair);
}
ジェネリクスとパフォーマンス
Rustのジェネリクスは単相化(Monomorphization)されます。
#![allow(unused)]
fn main() {
// このコードは
fn largest<T: PartialOrd>(a: T, b: T) -> T { ... }
largest(5, 10);
largest(1.0, 2.0);
// コンパイル時にこうなる
fn largest_i32(a: i32, b: i32) -> i32 { ... }
fn largest_f64(a: f64, b: f64) -> f64 { ... }
}
つまり、実行時のコストはゼロです!
標準ライブラリのジェネリクス例
#![allow(unused)]
fn main() {
// Vec<T>
let v: Vec<i32> = vec![1, 2, 3];
let v: Vec<String> = vec![String::from("a")];
// HashMap<K, V>
use std::collections::HashMap;
let mut map: HashMap<String, i32> = HashMap::new();
// Option<T>
let some: Option<i32> = Some(5);
// Result<T, E>
let result: Result<i32, String> = Ok(42);
}
まとめ
| 概念 | 説明 |
|---|---|
| ジェネリクス | 型をパラメータ化 |
<T> | 型パラメータ |
| トレイト境界 | T: Traitで制約を追加 |
where句 | 複雑な境界を読みやすく |
| 単相化 | コンパイル時に具体型に展開(ゼロコスト) |
確認テスト
Q1. ジェネリクスの主な目的は?
Vec<T>はVec<i32>にもVec<String>にもなれます。
Q2. fn foo<T: Clone + Debug>(x: T)のトレイト境界が意味するのは?
+は「かつ」を意味し、両方のトレイトを実装している型のみ受け付けます。
Q3. 以下のコードの出力は?struct Container<T> { value: T } impl<T> Container<T> { fn new(value: T) -> Self { Container { value } } fn get(&self) -> &T { &self.value } } let c = Container::new(42); println!("{}", c.get());
Container<i32>が作成され、getメソッドで内部の値への参照が返されます。println!は参照を自動的に解決して42を出力します。
Q4. fn print_largest<T>(a: T, b: T)でa > bを使うために必要なトレイト境界は?
>、<など)を使うにはPartialOrdトレイトが必要です。さらにprintln!で表示するにはDisplayも必要になります。
Q5. Rustのジェネリクスの「単相化(Monomorphization)」とは?
次のドキュメント: 05_modules.md
モジュールシステム
コードを整理する「モジュール」について学びます。
モジュールとは
モジュールはコードを論理的に分割する仕組みです。
my_project/
├── src/
│ ├── main.rs # エントリーポイント
│ ├── lib.rs # ライブラリクレートのルート
│ ├── utils.rs # utilsモジュール
│ └── models/
│ ├── mod.rs # modelsモジュールのルート
│ └── user.rs # models::userサブモジュール
基本的なモジュール定義
同一ファイル内
mod greetings {
pub fn hello() {
println!("Hello!");
}
fn private_function() {
println!("This is private");
}
}
fn main() {
greetings::hello();
// greetings::private_function(); // エラー!プライベート
}
別ファイルに分割
src/main.rs
mod utils; // src/utils.rs を読み込む
fn main() {
utils::greet();
}
src/utils.rs
#![allow(unused)]
fn main() {
pub fn greet() {
println!("Hello from utils!");
}
}
pub(公開)キーワード
デフォルトはプライベート。pubで公開します。
mod outer {
pub mod inner {
pub fn public_function() {
println!("公開関数");
}
fn private_function() {
println!("非公開関数");
}
}
}
fn main() {
outer::inner::public_function();
}
構造体フィールドの公開
mod models {
pub struct User {
pub name: String, // 公開
email: String, // 非公開
}
impl User {
pub fn new(name: String, email: String) -> User {
User { name, email }
}
pub fn email(&self) -> &str {
&self.email
}
}
}
fn main() {
let user = models::User::new(
String::from("太郎"),
String::from("taro@example.com"),
);
println!("名前: {}", user.name);
// println!("メール: {}", user.email); // エラー!非公開
println!("メール: {}", user.email()); // メソッド経由でOK
}
use キーワード
長いパスを短縮します。
mod models {
pub mod user {
pub struct User {
pub name: String,
}
}
}
// useでパスを短縮
use models::user::User;
fn main() {
let u = User { name: String::from("太郎") };
// models::user::User と書かなくてよい
}
複数のインポート
#![allow(unused)]
fn main() {
use std::collections::{HashMap, HashSet};
use std::io::{self, Read, Write};
}
エイリアス(as)
use std::collections::HashMap as Map;
fn main() {
let mut m: Map<String, i32> = Map::new();
}
再エクスポート(pub use)
#![allow(unused)]
fn main() {
mod models {
pub mod user {
pub struct User { pub name: String }
}
}
// 外部からmodels::Userでアクセス可能にする
pub use models::user::User;
}
ディレクトリ構造
方法1: mod.rs を使う
src/
├── main.rs
└── models/
├── mod.rs # モジュールのルート
└── user.rs
src/main.rs
#![allow(unused)]
fn main() {
mod models;
use models::User;
}
src/models/mod.rs
#![allow(unused)]
fn main() {
mod user;
pub use user::User;
}
src/models/user.rs
#![allow(unused)]
fn main() {
pub struct User {
pub name: String,
}
}
方法2: ファイル名でモジュール(Rust 2018以降)
src/
├── main.rs
├── models.rs # mod models の定義
└── models/
└── user.rs # mod models::user の定義
src/models.rs
#![allow(unused)]
fn main() {
pub mod user;
pub use user::User;
}
クレート(Crate)
クレートはRustのコンパイル単位です。
- バイナリクレート: 実行可能ファイル(
main.rs) - ライブラリクレート: 他のコードから使うライブラリ(
lib.rs)
外部クレートの使用
Cargo.toml
[dependencies]
rand = "0.8"
serde = { version = "1.0", features = ["derive"] }
src/main.rs
use rand::Rng;
fn main() {
let n = rand::thread_rng().gen_range(1..100);
println!("{}", n);
}
実践的なプロジェクト構造
my_app/
├── Cargo.toml
└── src/
├── main.rs # エントリーポイント
├── lib.rs # ライブラリ(オプション)
├── config.rs # 設定モジュール
├── error.rs # エラー定義
├── models/
│ ├── mod.rs
│ ├── user.rs
│ └── post.rs
└── handlers/
├── mod.rs
├── auth.rs
└── api.rs
まとめ
| 概念 | 説明 |
|---|---|
mod | モジュールを定義/インポート |
pub | 公開する(デフォルトは非公開) |
use | パスを短縮 |
pub use | 再エクスポート |
| クレート | コンパイル単位(binary/library) |
確認テスト
Q1. Rustのアイテム(関数、構造体など)のデフォルトの可視性は?
pubキーワードで明示的に公開する必要があります。
Q2. use std::collections::{HashMap, HashSet};は何をしている?
{}を使って、同じパスから複数のアイテムを一度にインポートしています。
Q3. 以下のコードがコンパイルエラーになる理由は?mod secret { fn hidden() { println!("Hidden!"); } } fn main() { secret::hidden(); }
hidden関数はpubがないため非公開です。モジュール外からアクセスするにはpub fn hidden()のように公開する必要があります。
Q4. pub useの役割は?
pub useは内部のアイテムを再エクスポートし、外部からより短いパスでアクセスできるようにします。
Q5. Rustにおける「クレート(Crate)」とは?
次のドキュメント: 06_error_handling_adv.md
エラーハンドリング応用
実践的なエラー処理のパターンを学びます。
? 演算子
エラーを呼び出し元に伝播させます。
#![allow(unused)]
fn main() {
use std::fs::File;
use std::io::{self, Read};
// ? を使わない場合
fn read_file_verbose(path: &str) -> Result<String, io::Error> {
let file = match File::open(path) {
Ok(f) => f,
Err(e) => return Err(e),
};
let mut contents = String::new();
match file.read_to_string(&mut contents) {
Ok(_) => Ok(contents),
Err(e) => Err(e),
}
}
// ? を使う場合(簡潔!)
fn read_file(path: &str) -> Result<String, io::Error> {
let mut file = File::open(path)?;
let mut contents = String::new();
file.read_to_string(&mut contents)?;
Ok(contents)
}
}
? の動作
#![allow(unused)]
fn main() {
let value = some_result?;
// は以下と同等
let value = match some_result {
Ok(v) => v,
Err(e) => return Err(e.into()),
};
}
チェーン
#![allow(unused)]
fn main() {
fn read_file(path: &str) -> Result<String, io::Error> {
let mut contents = String::new();
File::open(path)?.read_to_string(&mut contents)?;
Ok(contents)
}
// さらに簡潔に
fn read_file_short(path: &str) -> Result<String, io::Error> {
std::fs::read_to_string(path)
}
}
カスタムエラー型
基本的なカスタムエラー
#![allow(unused)]
fn main() {
#[derive(Debug)]
enum AppError {
NotFound(String),
InvalidInput(String),
IoError(std::io::Error),
}
impl std::fmt::Display for AppError {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
match self {
AppError::NotFound(msg) => write!(f, "Not found: {}", msg),
AppError::InvalidInput(msg) => write!(f, "Invalid input: {}", msg),
AppError::IoError(e) => write!(f, "IO error: {}", e),
}
}
}
impl std::error::Error for AppError {}
}
From トレイトで変換を自動化
#![allow(unused)]
fn main() {
impl From<std::io::Error> for AppError {
fn from(error: std::io::Error) -> Self {
AppError::IoError(error)
}
}
// これで ? が自動変換してくれる
fn do_something() -> Result<(), AppError> {
let _file = std::fs::File::open("file.txt")?; // io::Error -> AppError
Ok(())
}
}
thiserror クレート(推奨)
カスタムエラーを簡単に定義できます。
Cargo.toml
[dependencies]
thiserror = "1.0"
#![allow(unused)]
fn main() {
use thiserror::Error;
#[derive(Error, Debug)]
enum AppError {
#[error("User not found: {0}")]
NotFound(String),
#[error("Invalid input: {0}")]
InvalidInput(String),
#[error("Database error")]
DatabaseError(#[from] sqlx::Error),
#[error("IO error")]
IoError(#[from] std::io::Error),
}
}
anyhow クレート(アプリケーション向け)
エラー型を気にせず簡単にエラー処理できます。
Cargo.toml
[dependencies]
anyhow = "1.0"
use anyhow::{Result, Context};
fn read_config() -> Result<String> {
let content = std::fs::read_to_string("config.toml")
.context("設定ファイルの読み込みに失敗")?;
Ok(content)
}
fn main() -> Result<()> {
let config = read_config()?;
println!("{}", config);
Ok(())
}
エラー処理のパターン
パターン1: 早期リターン
#![allow(unused)]
fn main() {
fn process_user(id: u32) -> Result<User, AppError> {
if id == 0 {
return Err(AppError::InvalidInput("ID cannot be 0".into()));
}
let user = find_user(id)?;
if !user.is_active {
return Err(AppError::NotFound("User is inactive".into()));
}
Ok(user)
}
}
パターン2: map_err で変換
#![allow(unused)]
fn main() {
fn read_config() -> Result<Config, AppError> {
let content = std::fs::read_to_string("config.toml")
.map_err(|e| AppError::IoError(e))?;
toml::from_str(&content)
.map_err(|e| AppError::InvalidInput(e.to_string()))
}
}
パターン3: ok_or / ok_or_else
#![allow(unused)]
fn main() {
fn get_user(id: u32) -> Result<User, AppError> {
users.get(&id)
.cloned()
.ok_or_else(|| AppError::NotFound(format!("User {} not found", id)))
}
}
パターン4: and_then でチェーン
#![allow(unused)]
fn main() {
fn process() -> Result<String, AppError> {
read_file("input.txt")
.and_then(|content| parse_content(&content))
.and_then(|data| transform_data(data))
.and_then(|result| save_result(result))
}
}
main関数でのエラー処理
use std::process;
fn main() {
if let Err(e) = run() {
eprintln!("エラー: {}", e);
process::exit(1);
}
}
fn run() -> Result<(), Box<dyn std::error::Error>> {
let config = read_config()?;
let data = process_data(&config)?;
save_output(&data)?;
Ok(())
}
パニックとの使い分け
| 状況 | 使うもの |
|---|---|
| 回復可能なエラー | Result<T, E> |
| プログラムのバグ | panic! |
| テスト | unwrap(), expect() |
| プロトタイプ | unwrap() |
| 本番コード | 適切なエラー処理 |
#![allow(unused)]
fn main() {
// panic! を使う場面(プログラムのバグ)
fn get_element(index: usize) -> &str {
let items = ["a", "b", "c"];
items.get(index).expect("Index should be valid")
}
// Result を使う場面(回復可能なエラー)
fn find_user(id: u32) -> Result<User, AppError> {
// ...
}
}
まとめ
| 概念 | 説明 |
|---|---|
? | エラーを伝播 |
| カスタムエラー | アプリ固有のエラー型 |
thiserror | エラー型定義を簡単に |
anyhow | 柔軟なエラー処理 |
map_err | エラー型を変換 |
context | エラーに文脈を追加 |
確認テスト
Q1. ?演算子の動作は?
?はResultがErrの場合、そのエラーを関数からreturnします。Okの場合は中の値を取り出します。
Q2. thiserrorとanyhowの使い分けは?
thiserrorは具体的なエラー型を定義するのに適しており、ライブラリで使います。anyhowは様々なエラーを簡単に扱えるため、アプリケーションで使います。
Q3. map_errの役割は?
map_errはResultのErrの値を別の型に変換します。異なるエラー型を統一するのに便利です。
Q4. 以下のコードを?で簡潔にした結果は?let content = match std::fs::read_to_string(path) { Ok(c) => c, Err(e) => return Err(e) };
?演算子はOkの場合は値を取り出し、Errの場合は早期リターンします。まさにmatchで書いていた処理と同等です。
Q5. panic!を使うべき場面は?
panic!はプログラムのバグなど回復不能な状態で使います。ファイルエラーやネットワークエラー、ユーザー入力エラーはResultで適切に処理すべきです。
Phase 3 完了!
おめでとうございます!Phase 3を完了しました。
学んだこと:
- 構造体(struct、impl)
- 列挙型とパターンマッチング
- トレイト
- ジェネリクス
- モジュールシステム
- エラーハンドリング応用
次のPhase: Phase 4: エコシステムと実践
Cargo活用
Rustのビルドツール「Cargo」の高度な機能を学びます。
Cargo.tomlの詳細
基本構造
[package]
name = "my_app"
version = "0.1.0"
edition = "2021"
authors = ["Your Name <you@example.com>"]
description = "My awesome application"
license = "MIT"
[dependencies]
serde = "1.0"
[dev-dependencies]
criterion = "0.4"
[build-dependencies]
cc = "1.0"
バージョン指定
[dependencies]
# 完全一致
exact = "=1.0.0"
# キャレット(デフォルト): 互換性のある最新
caret = "1.0" # >=1.0.0, <2.0.0
caret2 = "1.0.0" # >=1.0.0, <2.0.0
# チルダ: マイナーバージョンまで固定
tilde = "~1.0" # >=1.0.0, <1.1.0
# ワイルドカード
wildcard = "1.*" # >=1.0.0, <2.0.0
# 範囲
range = ">=1.0, <2.0"
Features(機能フラグ)
依存クレートのfeatures
[dependencies]
serde = { version = "1.0", features = ["derive"] }
tokio = { version = "1", features = ["full"] }
# または一部のみ
tokio = { version = "1", features = ["rt", "net", "io-util"] }
自分のクレートにfeatures定義
[features]
default = ["json"]
json = ["dep:serde_json"]
yaml = ["dep:serde_yaml"]
full = ["json", "yaml"]
[dependencies]
serde_json = { version = "1.0", optional = true }
serde_yaml = { version = "0.9", optional = true }
#![allow(unused)]
fn main() {
#[cfg(feature = "json")]
pub fn parse_json(s: &str) -> Result<Value, Error> {
serde_json::from_str(s)
}
}
ビルドプロファイル
dev(開発用)
[profile.dev]
opt-level = 0 # 最適化なし
debug = true # デバッグ情報あり
release(本番用)
[profile.release]
opt-level = 3 # 最大最適化
debug = false # デバッグ情報なし
lto = true # リンク時最適化
カスタムプロファイル
[profile.bench]
opt-level = 3
debug = true
[profile.profiling]
inherits = "release"
debug = true
ワークスペース
複数のクレートをまとめて管理します。
my_workspace/
├── Cargo.toml # ワークスペースルート
├── app/
│ ├── Cargo.toml
│ └── src/
├── lib_core/
│ ├── Cargo.toml
│ └── src/
└── lib_utils/
├── Cargo.toml
└── src/
ルートCargo.toml
[workspace]
members = ["app", "lib_core", "lib_utils"]
app/Cargo.toml
[package]
name = "app"
version = "0.1.0"
[dependencies]
lib_core = { path = "../lib_core" }
lib_utils = { path = "../lib_utils" }
便利なCargoコマンド
| コマンド | 説明 |
|---|---|
cargo build | ビルド |
cargo build --release | リリースビルド |
cargo run | ビルド+実行 |
cargo check | 型チェックのみ(高速) |
cargo test | テスト実行 |
cargo doc --open | ドキュメント生成&表示 |
cargo fmt | コード整形 |
cargo clippy | 静的解析 |
cargo update | 依存関係更新 |
cargo tree | 依存関係ツリー表示 |
Cargo.lock
依存関係の正確なバージョンを記録します。
- ライブラリ: .gitignoreに入れる(利用者が決める)
- アプリケーション: コミットする(再現性のため)
環境変数
// ビルド時の情報
const VERSION: &str = env!("CARGO_PKG_VERSION");
const NAME: &str = env!("CARGO_PKG_NAME");
fn main() {
println!("{} v{}", NAME, VERSION);
}
まとめ
| 機能 | 用途 |
|---|---|
| features | オプション機能の切り替え |
| profiles | 最適化レベルの設定 |
| workspace | 複数クレートの管理 |
cargo check | 高速な型チェック |
cargo clippy | 静的解析 |
確認テスト
Q1. `cargo check`と`cargo build`の違いは?
Q2. `[dependencies]`と`[dev-dependencies]`の違いは?
Q3. 以下のCargo.tomlで`serde`のどのfeaturesが有効になる?serde = { version = "1.0", features = ["derive", "rc"] }
Q4. 以下のCargo.tomlの問題は何?tokio = "1", features = ["full"]
Q5. ワークスペースのルートCargo.tomlで正しい記述は?
次のドキュメント: 02_crate_ecosystem.md
クレートエコシステム
Rustの豊富なライブラリ(クレート)を活用する方法を学びます。
crates.io
crates.io はRustの公式パッケージレジストリです。
クレートを探す
- crates.ioで検索
- lib.rs で分類別に探す
- GitHubのAwesome Rustリストを参照
良いクレートの選び方
| 指標 | 確認ポイント |
|---|---|
| ダウンロード数 | 多いほど信頼性が高い傾向 |
| 最終更新 | 活発にメンテナンスされているか |
| ドキュメント | docs.rsで確認 |
| 依存関係 | 少ないほどシンプル |
| ライセンス | MIT/Apache 2.0が一般的 |
主要クレート紹介
シリアライゼーション: serde
[dependencies]
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
use serde::{Deserialize, Serialize};
#[derive(Serialize, Deserialize, Debug)]
struct User {
name: String,
age: u32,
}
fn main() {
let user = User {
name: String::from("太郎"),
age: 25,
};
// JSON化
let json = serde_json::to_string(&user).unwrap();
println!("{}", json);
// JSONからパース
let parsed: User = serde_json::from_str(&json).unwrap();
println!("{:?}", parsed);
}
HTTPクライアント: reqwest
[dependencies]
reqwest = { version = "0.11", features = ["json"] }
tokio = { version = "1", features = ["full"] }
use reqwest;
#[tokio::main]
async fn main() -> Result<(), reqwest::Error> {
let body = reqwest::get("https://httpbin.org/get")
.await?
.text()
.await?;
println!("{}", body);
Ok(())
}
非同期ランタイム: tokio
[dependencies]
tokio = { version = "1", features = ["full"] }
use tokio::time::{sleep, Duration};
#[tokio::main]
async fn main() {
println!("開始");
sleep(Duration::from_secs(1)).await;
println!("1秒後");
}
CLI引数パース: clap
[dependencies]
clap = { version = "4", features = ["derive"] }
use clap::Parser;
#[derive(Parser, Debug)]
#[command(name = "myapp")]
#[command(about = "サンプルアプリケーション")]
struct Args {
/// 名前
#[arg(short, long)]
name: String,
/// 回数
#[arg(short, long, default_value_t = 1)]
count: u8,
}
fn main() {
let args = Args::parse();
for _ in 0..args.count {
println!("Hello, {}!", args.name);
}
}
ログ: tracing
[dependencies]
tracing = "0.1"
tracing-subscriber = "0.3"
use tracing::{info, warn, error, debug};
use tracing_subscriber;
fn main() {
tracing_subscriber::fmt::init();
info!("アプリケーション開始");
debug!("デバッグ情報");
warn!("警告");
error!("エラー");
}
日時: chrono
[dependencies]
chrono = "0.4"
use chrono::{Local, Utc};
fn main() {
let now = Local::now();
println!("現在時刻: {}", now.format("%Y-%m-%d %H:%M:%S"));
let utc = Utc::now();
println!("UTC: {}", utc);
}
正規表現: regex
[dependencies]
regex = "1"
use regex::Regex;
fn main() {
let re = Regex::new(r"\d{3}-\d{4}").unwrap();
let text = "郵便番号: 123-4567";
if let Some(m) = re.find(text) {
println!("見つかりました: {}", m.as_str());
}
}
カテゴリ別おすすめクレート
Web開発
| クレート | 用途 |
|---|---|
| axum | Webフレームワーク |
| actix-web | 高性能Webフレームワーク |
| tower | ミドルウェア |
データベース
| クレート | 用途 |
|---|---|
| sqlx | 非同期SQL |
| diesel | ORM |
| sea-orm | 非同期ORM |
エラー処理
| クレート | 用途 |
|---|---|
| thiserror | カスタムエラー定義 |
| anyhow | 柔軟なエラー処理 |
テスト
| クレート | 用途 |
|---|---|
| mockall | モック |
| proptest | プロパティベーステスト |
まとめ
| カテゴリ | 推奨クレート |
|---|---|
| シリアライズ | serde + serde_json |
| HTTP | reqwest |
| 非同期 | tokio |
| CLI | clap |
| ログ | tracing |
| 日時 | chrono |
| 正規表現 | regex |
確認テスト
Q1. serdeの主な用途は?
Q2. tokioの役割は?
Q3. `#[derive(Serialize, Deserialize)]`を構造体に付けると何ができる?
Q4. `#[derive(Serialize, Deserialize)]`を使うために必要なCargo.tomlの設定は?
Q5. chronoクレートで現在時刻を"2024-01-15 10:30:00"形式で表示するフォーマット文字列は?
次のドキュメント: 03_testing.md
テスト
Rustの組み込みテスト機能を学びます。
単体テスト
基本的なテスト
#![allow(unused)]
fn main() {
fn add(a: i32, b: i32) -> i32 {
a + b
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_add() {
assert_eq!(add(2, 3), 5);
}
#[test]
fn test_add_negative() {
assert_eq!(add(-1, 1), 0);
}
}
}
実行:
cargo test
アサーションマクロ
#![allow(unused)]
fn main() {
#[test]
fn test_assertions() {
// 等値チェック
assert_eq!(1 + 1, 2);
// 不等値チェック
assert_ne!(1 + 1, 3);
// 条件チェック
assert!(true);
assert!(!false);
// カスタムメッセージ
assert_eq!(1 + 1, 2, "1 + 1 は 2 のはず");
}
}
パニックのテスト
#![allow(unused)]
fn main() {
fn divide(a: i32, b: i32) -> i32 {
if b == 0 {
panic!("ゼロ除算!");
}
a / b
}
#[test]
#[should_panic]
fn test_divide_by_zero() {
divide(10, 0);
}
#[test]
#[should_panic(expected = "ゼロ除算")]
fn test_divide_by_zero_message() {
divide(10, 0);
}
}
Result を返すテスト
#![allow(unused)]
fn main() {
#[test]
fn test_with_result() -> Result<(), String> {
if 2 + 2 == 4 {
Ok(())
} else {
Err(String::from("計算が間違っています"))
}
}
}
テストの構成
テストモジュール
#![allow(unused)]
fn main() {
// src/lib.rs
pub fn public_function() -> i32 {
private_function() + 1
}
fn private_function() -> i32 {
42
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_public() {
assert_eq!(public_function(), 43);
}
#[test]
fn test_private() {
// 同じモジュール内なのでプライベート関数もテスト可能
assert_eq!(private_function(), 42);
}
}
}
テストのフィルタリング
# すべてのテスト
cargo test
# 特定のテストのみ
cargo test test_add
# 特定のモジュールのテスト
cargo test tests::
# 無視されたテストを実行
cargo test -- --ignored
テストの無視
#![allow(unused)]
fn main() {
#[test]
#[ignore]
fn expensive_test() {
// 時間のかかるテスト
}
}
統合テスト
プロジェクト全体をテストします。
my_project/
├── src/
│ └── lib.rs
└── tests/
└── integration_test.rs
tests/integration_test.rs
#![allow(unused)]
fn main() {
use my_project;
#[test]
fn test_integration() {
assert_eq!(my_project::public_function(), 43);
}
}
テストのベストプラクティス
1. AAAパターン
#![allow(unused)]
fn main() {
#[test]
fn test_user_creation() {
// Arrange(準備)
let name = "太郎";
let age = 25;
// Act(実行)
let user = User::new(name.to_string(), age);
// Assert(検証)
assert_eq!(user.name, "太郎");
assert_eq!(user.age, 25);
}
}
2. 境界値テスト
#![allow(unused)]
fn main() {
#[test]
fn test_is_adult() {
assert!(!is_adult(17)); // 境界の下
assert!(is_adult(18)); // 境界
assert!(is_adult(19)); // 境界の上
}
}
3. エラーケースのテスト
#![allow(unused)]
fn main() {
#[test]
fn test_parse_error() {
let result = parse_number("not a number");
assert!(result.is_err());
}
#[test]
fn test_parse_success() {
let result = parse_number("42");
assert_eq!(result.unwrap(), 42);
}
}
テストヘルパー
共通のセットアップ
#![allow(unused)]
fn main() {
#[cfg(test)]
mod tests {
use super::*;
fn setup() -> User {
User::new("テスト太郎".to_string(), 20)
}
#[test]
fn test_user_name() {
let user = setup();
assert_eq!(user.name, "テスト太郎");
}
#[test]
fn test_user_age() {
let user = setup();
assert_eq!(user.age, 20);
}
}
}
テストカバレッジ
# cargo-tarpaulinをインストール
cargo install cargo-tarpaulin
# カバレッジレポート
cargo tarpaulin
まとめ
| 機能 | 説明 |
|---|---|
#[test] | テスト関数を示す |
assert! | 条件がtrueか検証 |
assert_eq! | 等値を検証 |
assert_ne! | 不等値を検証 |
#[should_panic] | パニックを期待 |
#[ignore] | テストを無視 |
#[cfg(test)] | テスト時のみコンパイル |
確認テスト
Q1. `assert_eq!(a, b)`と`assert_ne!(a, b)`の違いは?
Q2. `#[should_panic]`の用途は?
Q3. 以下のテストの結果は?#[test] fn test() { assert_eq!(2 + 2, 5); }
Q4. `assert!(is_even(3))`が失敗する理由は?(is_evenは偶数でtrueを返す関数)
Q5. 統合テストを配置する正しいディレクトリは?
次のドキュメント: 04_documentation.md
ドキュメンテーション
Rustのドキュメント機能を学びます。
ドキュメントコメント
アイテムのドキュメント(///)
#![allow(unused)]
fn main() {
/// 2つの数値を加算します。
///
/// # Arguments
///
/// * `a` - 1つ目の数値
/// * `b` - 2つ目の数値
///
/// # Returns
///
/// 2つの数値の合計
///
/// # Examples
///
/// ```
/// let result = add(2, 3);
/// assert_eq!(result, 5);
/// ```
pub fn add(a: i32, b: i32) -> i32 {
a + b
}
}
モジュール/クレートのドキュメント(//!)
#![allow(unused)]
fn main() {
//! # My Crate
//!
//! `my_crate` は便利な機能を提供するクレートです。
//!
//! ## 使い方
//!
//! ```rust
//! use my_crate::add;
//! let sum = add(1, 2);
//! ```
/// 加算関数
pub fn add(a: i32, b: i32) -> i32 {
a + b
}
}
ドキュメントのセクション
よく使うセクション
#![allow(unused)]
fn main() {
/// 短い説明。
///
/// 詳しい説明をここに書きます。
/// 複数行にわたって書けます。
///
/// # Arguments
///
/// 引数の説明
///
/// # Returns
///
/// 戻り値の説明
///
/// # Errors
///
/// エラーが発生する条件
///
/// # Panics
///
/// パニックする条件
///
/// # Examples
///
/// 使用例
///
/// # Safety
///
/// unsafeな理由(unsafe関数の場合)
pub fn some_function() {}
}
ドキュメント生成
# ドキュメント生成
cargo doc
# 生成してブラウザで開く
cargo doc --open
# 依存クレートも含める
cargo doc --document-private-items
ドキュメントテスト
Examplesに書いたコードは自動的にテストされます。
#![allow(unused)]
fn main() {
/// 除算を行います。
///
/// # Examples
///
/// ```
/// let result = divide(10, 2);
/// assert_eq!(result, Some(5));
/// ```
///
/// ゼロで割るとNoneを返します:
///
/// ```
/// let result = divide(10, 0);
/// assert_eq!(result, None);
/// ```
pub fn divide(a: i32, b: i32) -> Option<i32> {
if b == 0 {
None
} else {
Some(a / b)
}
}
}
# ドキュメントテストを実行
cargo test --doc
テストを隠す
#![allow(unused)]
fn main() {
/// # Examples
///
/// ```
/// # // この行はドキュメントに表示されない
/// # fn setup() -> i32 { 42 }
/// let value = setup();
/// assert_eq!(value, 42);
/// ```
}
コンパイルのみ(実行しない)
#![allow(unused)]
fn main() {
/// ```no_run
/// // コンパイルはするが実行しない
/// std::process::exit(1);
/// ```
}
コンパイルエラーを期待
#![allow(unused)]
fn main() {
/// ```compile_fail
/// let x: i32 = "hello";
/// ```
}
構造体のドキュメント
#![allow(unused)]
fn main() {
/// ユーザーを表す構造体。
///
/// # Examples
///
/// ```
/// use my_crate::User;
///
/// let user = User::new("太郎".to_string(), 25);
/// assert_eq!(user.name(), "太郎");
/// ```
pub struct User {
/// ユーザー名
name: String,
/// 年齢
age: u32,
}
impl User {
/// 新しいユーザーを作成します。
///
/// # Arguments
///
/// * `name` - ユーザー名
/// * `age` - 年齢
pub fn new(name: String, age: u32) -> Self {
Self { name, age }
}
/// ユーザー名を返します。
pub fn name(&self) -> &str {
&self.name
}
}
}
リンク
#![allow(unused)]
fn main() {
/// [`Vec`]を使った例。
///
/// 詳細は[`std::collections::HashMap`]を参照。
///
/// [`add`]関数も参照してください。
pub fn example() {}
/// 加算関数
pub fn add(a: i32, b: i32) -> i32 {
a + b
}
}
ドキュメントのベストプラクティス
1. 最初の行は簡潔に
#![allow(unused)]
fn main() {
/// ファイルを読み込んで内容を返します。
///
/// 詳しい説明...
}
2. Examplesは動作するコードを
#![allow(unused)]
fn main() {
/// # Examples
///
/// ```
/// use my_crate::Config;
///
/// let config = Config::default();
/// assert!(config.is_valid());
/// ```
}
3. 公開APIは必ずドキュメント化
#![allow(unused)]
#![deny(missing_docs)] // ドキュメントがないとエラー
fn main() {
/// 公開関数
pub fn public_function() {}
}
まとめ
| 構文 | 用途 |
|---|---|
/// | アイテムのドキュメント |
//! | モジュール/クレートのドキュメント |
# Examples | 使用例(自動テスト) |
cargo doc | ドキュメント生成 |
cargo test --doc | ドキュメントテスト |
確認テスト
Q1. `///`と`//!`の違いは?
Q2. ドキュメントの`# Examples`セクションの特徴は?
Q3. コードブロックに付ける`no_run`の意味は?
Q4. `double(5)`が10を返す関数で、`assert_eq!(double(5), 11)`のドキュメントテストが失敗する理由は?
Q5. ドキュメントを生成してブラウザで開くコマンドは?
次のドキュメント: 05_async_basics.md
非同期プログラミング入門
Rustの非同期処理(async/await)の基礎を学びます。
なぜ非同期処理が必要か
同期処理の問題
#![allow(unused)]
fn main() {
// 同期処理:1つずつ順番に実行
fn fetch_data() {
let data1 = fetch_from_server1(); // 2秒待つ
let data2 = fetch_from_server2(); // 2秒待つ
let data3 = fetch_from_server3(); // 2秒待つ
// 合計: 6秒
}
}
非同期処理の利点
#![allow(unused)]
fn main() {
// 非同期処理:待ち時間を有効活用
async fn fetch_data() {
let (data1, data2, data3) = tokio::join!(
fetch_from_server1(),
fetch_from_server2(),
fetch_from_server3(),
);
// 合計: 約2秒(並行実行)
}
}
async/await の基本
async関数の定義
#![allow(unused)]
fn main() {
// async関数はFutureを返す
async fn hello() -> String {
"Hello, async world!".to_string()
}
// 上記は以下と同等
fn hello() -> impl Future<Output = String> {
async {
"Hello, async world!".to_string()
}
}
}
awaitで結果を取得
async fn greet(name: &str) -> String {
format!("Hello, {}!", name)
}
async fn main_async() {
let greeting = greet("Rust").await; // .awaitで結果を取得
println!("{}", greeting);
}
Tokioランタイム
非同期コードを実行するには「ランタイム」が必要です。
セットアップ
Cargo.toml
[dependencies]
tokio = { version = "1", features = ["full"] }
基本的な使い方
use tokio;
#[tokio::main]
async fn main() {
println!("非同期処理を開始");
let result = async_task().await;
println!("結果: {}", result);
}
async fn async_task() -> i32 {
// 非同期処理
42
}
#[tokio::main]の意味
// これは...
#[tokio::main]
async fn main() {
// 非同期コード
}
// こう展開される
fn main() {
tokio::runtime::Runtime::new()
.unwrap()
.block_on(async {
// 非同期コード
})
}
非同期の待機
tokio::time::sleep
use tokio::time::{sleep, Duration};
async fn delayed_hello() {
println!("3秒後に挨拶します...");
sleep(Duration::from_secs(3)).await;
println!("Hello!");
}
#[tokio::main]
async fn main() {
delayed_hello().await;
}
注意: std::thread::sleepとの違い
#![allow(unused)]
fn main() {
use std::thread;
use tokio::time::{sleep, Duration};
// ❌ これはスレッド全体をブロック
async fn bad_sleep() {
thread::sleep(Duration::from_secs(1)); // 他のタスクも止まる
}
// ✅ これは他のタスクに処理を譲る
async fn good_sleep() {
sleep(Duration::from_secs(1)).await; // 他のタスクは動ける
}
}
並行実行
tokio::join! - 複数のFutureを並行実行
use tokio::time::{sleep, Duration};
async fn task1() -> i32 {
sleep(Duration::from_secs(1)).await;
println!("Task 1 完了");
1
}
async fn task2() -> i32 {
sleep(Duration::from_secs(2)).await;
println!("Task 2 完了");
2
}
#[tokio::main]
async fn main() {
let start = std::time::Instant::now();
// 並行実行(合計約2秒)
let (result1, result2) = tokio::join!(task1(), task2());
println!("結果: {}, {}", result1, result2);
println!("経過時間: {:?}", start.elapsed());
}
tokio::select! - 最初に完了したものを処理
use tokio::time::{sleep, Duration};
async fn fast_task() -> &'static str {
sleep(Duration::from_millis(100)).await;
"fast"
}
async fn slow_task() -> &'static str {
sleep(Duration::from_secs(10)).await;
"slow"
}
#[tokio::main]
async fn main() {
tokio::select! {
result = fast_task() => {
println!("Fast finished: {}", result);
}
result = slow_task() => {
println!("Slow finished: {}", result);
}
}
// "Fast finished: fast" と表示(slowはキャンセル)
}
非同期でHTTPリクエスト
reqwestクレートを使用
Cargo.toml
[dependencies]
tokio = { version = "1", features = ["full"] }
reqwest = { version = "0.11", features = ["json"] }
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
基本的なGETリクエスト
use reqwest;
#[tokio::main]
async fn main() -> Result<(), reqwest::Error> {
let response = reqwest::get("https://httpbin.org/get")
.await?
.text()
.await?;
println!("Response: {}", response);
Ok(())
}
JSONレスポンスの解析
use reqwest;
use serde::Deserialize;
#[derive(Debug, Deserialize)]
struct ApiResponse {
origin: String,
url: String,
}
#[tokio::main]
async fn main() -> Result<(), reqwest::Error> {
let response: ApiResponse = reqwest::get("https://httpbin.org/get")
.await?
.json()
.await?;
println!("Origin: {}", response.origin);
println!("URL: {}", response.url);
Ok(())
}
複数のAPIを並行で呼び出し
use reqwest;
async fn fetch_url(url: &str) -> Result<String, reqwest::Error> {
let response = reqwest::get(url).await?.text().await?;
Ok(response)
}
#[tokio::main]
async fn main() -> Result<(), reqwest::Error> {
let urls = vec![
"https://httpbin.org/get",
"https://httpbin.org/ip",
"https://httpbin.org/user-agent",
];
// 並行でフェッチ
let futures: Vec<_> = urls.iter()
.map(|url| fetch_url(url))
.collect();
let results = futures::future::join_all(futures).await;
for (url, result) in urls.iter().zip(results) {
match result {
Ok(body) => println!("{}: {} bytes", url, body.len()),
Err(e) => println!("{}: エラー - {}", url, e),
}
}
Ok(())
}
エラーハンドリング
Result と ?演算子
use reqwest;
use thiserror::Error;
#[derive(Error, Debug)]
enum AppError {
#[error("HTTPエラー: {0}")]
Http(#[from] reqwest::Error),
#[error("JSONパースエラー: {0}")]
Json(#[from] serde_json::Error),
}
async fn fetch_data(url: &str) -> Result<String, AppError> {
let response = reqwest::get(url).await?;
let text = response.text().await?;
Ok(text)
}
#[tokio::main]
async fn main() {
match fetch_data("https://httpbin.org/get").await {
Ok(data) => println!("成功: {} bytes", data.len()),
Err(e) => eprintln!("エラー: {}", e),
}
}
タイムアウト
use tokio::time::{timeout, Duration};
use reqwest;
#[tokio::main]
async fn main() {
let result = timeout(
Duration::from_secs(5),
reqwest::get("https://httpbin.org/delay/10")
).await;
match result {
Ok(Ok(response)) => println!("成功: {}", response.status()),
Ok(Err(e)) => println!("リクエストエラー: {}", e),
Err(_) => println!("タイムアウト!"),
}
}
非同期のベストプラクティス
1. ブロッキング処理を避ける
#![allow(unused)]
fn main() {
// ❌ 悪い例
async fn bad_example() {
std::thread::sleep(Duration::from_secs(1)); // ブロック
std::fs::read_to_string("file.txt"); // ブロック
}
// ✅ 良い例
async fn good_example() {
tokio::time::sleep(Duration::from_secs(1)).await;
tokio::fs::read_to_string("file.txt").await;
}
}
2. 重い処理は spawn_blocking
#![allow(unused)]
fn main() {
use tokio::task;
async fn compute_heavy() -> i32 {
// CPU重い処理は別スレッドで
task::spawn_blocking(|| {
// 重い計算
(0..1_000_000).sum()
}).await.unwrap()
}
}
3. エラーは早めに処理
#![allow(unused)]
fn main() {
async fn fetch_and_process(url: &str) -> Result<String, Error> {
let response = reqwest::get(url).await?;
// ステータスチェック
if !response.status().is_success() {
return Err(Error::HttpStatus(response.status()));
}
let text = response.text().await?;
Ok(text)
}
}
まとめ
| 概念 | 説明 |
|---|---|
async | 非同期関数を定義 |
.await | Futureの完了を待つ |
tokio | 非同期ランタイム |
join! | 複数を並行実行 |
select! | 最初の完了を処理 |
| 注意点 | 対策 |
|---|---|
| ブロッキング | tokio::time::sleep等を使用 |
| CPU重い処理 | spawn_blockingを使用 |
| エラー処理 | ?演算子とResultを活用 |
確認テスト
Q1. async関数が返すものは?
async関数は呼び出しただけでは実行されません。Futureを返し、.awaitされたときに初めて実行されます。
Q2. tokio::join!(task1(), task2())の動作は?
join!は複数のFutureを並行に実行し、すべてが完了するまで待ちます。各タスクの待ち時間を有効活用できます。
Q3. 以下のコードの実行時間は約何秒?tokio::join!(task_a(), task_b())(task_aは2秒sleep、task_bは3秒sleep)
join!は両方のタスクを並行実行します。task_aは2秒、task_bは3秒かかりますが、並行なので最も長い3秒で両方完了します(順次実行なら5秒かかる)。
Q4. 非同期関数内でstd::thread::sleepを使うとどうなる?
std::thread::sleepはスレッド全体をブロックするため、非同期ランタイム上の他のタスクも止まってしまいます。非同期コード内ではtokio::time::sleepを使うべきです。
Q5. tokio::select!の動作として正しいのは?
select!は複数のFutureを並行に実行し、最初に完了したものの結果を処理します。他の未完了のFutureはキャンセルされます。タイムアウト処理などに便利です。
Phase 4 完了!
おめでとうございます!Phase 4を完了しました。
学んだこと:
- Cargoの高度な機能(ワークスペース、features)
- クレートエコシステムの活用
- テストの書き方(単体テスト、統合テスト)
- ドキュメンテーション
- 非同期プログラミング(async/await、tokio)
次のPhase: Phase 5: Webアプリ開発
Web基礎
Webアプリケーション開発の基礎概念を学びます。
Webの仕組み
クライアントとサーバー
┌─────────────┐ HTTP ┌─────────────┐
│ クライアント │ ──── リクエスト ────→ │ サーバー │
│ (ブラウザ) │ ←──── レスポンス ──── │ (Rustアプリ) │
└─────────────┘ └─────────────┘
- クライアント: ブラウザやアプリ。リクエストを送信
- サーバー: リクエストを受け取り、レスポンスを返す
HTTPプロトコル
HTTPリクエスト
GET /users/123 HTTP/1.1
Host: api.example.com
Content-Type: application/json
Authorization: Bearer token123
{リクエストボディ}
構成要素:
- メソッド: GET, POST, PUT, DELETE など
- パス:
/users/123 - ヘッダー: メタ情報
- ボディ: 送信データ(POST/PUTなど)
HTTPレスポンス
HTTP/1.1 200 OK
Content-Type: application/json
{"id": 123, "name": "Taro"}
構成要素:
- ステータスコード: 200, 404, 500 など
- ヘッダー: メタ情報
- ボディ: レスポンスデータ
HTTPメソッド
| メソッド | 用途 | 例 |
|---|---|---|
| GET | データ取得 | ユーザー情報を取得 |
| POST | データ作成 | 新規ユーザー登録 |
| PUT | データ更新(全体) | ユーザー情報を完全置換 |
| PATCH | データ更新(部分) | メールアドレスのみ更新 |
| DELETE | データ削除 | ユーザーを削除 |
ステータスコード
2xx: 成功
| コード | 意味 | 用途 |
|---|---|---|
| 200 | OK | 成功(一般的) |
| 201 | Created | リソース作成成功 |
| 204 | No Content | 成功(レスポンスボディなし) |
4xx: クライアントエラー
| コード | 意味 | 用途 |
|---|---|---|
| 400 | Bad Request | リクエストが不正 |
| 401 | Unauthorized | 認証が必要 |
| 403 | Forbidden | アクセス権限なし |
| 404 | Not Found | リソースが見つからない |
| 422 | Unprocessable Entity | バリデーションエラー |
5xx: サーバーエラー
| コード | 意味 | 用途 |
|---|---|---|
| 500 | Internal Server Error | サーバー内部エラー |
| 502 | Bad Gateway | 上流サーバーエラー |
| 503 | Service Unavailable | サービス利用不可 |
REST API
RESTful APIは、HTTPを使ってリソースを操作する設計パターンです。
REST設計の原則
- リソース指向: URLはリソースを表す(動詞ではなく名詞)
- HTTPメソッドで操作を表現: GET/POST/PUT/DELETE
- ステートレス: 各リクエストは独立
REST APIの例
# ユーザー一覧を取得
GET /users
# 特定のユーザーを取得
GET /users/123
# 新しいユーザーを作成
POST /users
Body: {"name": "Taro", "email": "taro@example.com"}
# ユーザーを更新
PUT /users/123
Body: {"name": "Taro", "email": "new@example.com"}
# ユーザーを削除
DELETE /users/123
URLの設計
# 良い例(名詞、複数形)
GET /users
GET /users/123
GET /users/123/posts
GET /posts?author=123
# 悪い例(動詞が入っている)
GET /getUsers
POST /createUser
GET /getUserById/123
JSON
Web APIでよく使われるデータ形式です。
JSON形式
{
"id": 123,
"name": "Taro",
"email": "taro@example.com",
"age": 25,
"active": true,
"tags": ["rust", "web"],
"address": {
"city": "Tokyo",
"zip": "100-0001"
}
}
RustでのJSON処理(serde)
#![allow(unused)]
fn main() {
use serde::{Deserialize, Serialize};
#[derive(Debug, Serialize, Deserialize)]
struct User {
id: u32,
name: String,
email: String,
}
// JSONからRustの構造体へ
let json = r#"{"id": 1, "name": "Taro", "email": "taro@example.com"}"#;
let user: User = serde_json::from_str(json).unwrap();
// Rustの構造体からJSONへ
let json_output = serde_json::to_string(&user).unwrap();
}
Webアプリケーションの種類
1. APIサーバー(バックエンド)
クライアント ──JSON── APIサーバー ── データベース
- JSONでデータをやり取り
- フロントエンドと分離
- 今回学ぶのはこれ
2. サーバーサイドレンダリング(SSR)
ブラウザ ←─HTML─ サーバー ── データベース
- サーバーでHTMLを生成
- 従来型のWebアプリ
3. 静的サイト + API
ブラウザ ←─HTML/JS─ 静的ファイル
│
└──JSON── APIサーバー
- HTML/JSは静的配信
- データはAPIから取得
Rustでの Web開発
主要なWebフレームワーク
| フレームワーク | 特徴 |
|---|---|
| Axum | モダン、tokio製、型安全 |
| Actix-web | 高性能、成熟 |
| Rocket | 使いやすさ重視 |
| Warp | シンプル、コンポーザブル |
このカリキュラムでは Axum を使用します。
なぜAxumか
- tokioチームが開発(非同期との相性が良い)
- 型システムを活かした安全な設計
- モダンなRustの機能を活用
- 活発なコミュニティ
まとめ
| 概念 | 説明 |
|---|---|
| HTTP | クライアント-サーバー間の通信プロトコル |
| REST | リソース指向のAPI設計パターン |
| JSON | データ交換フォーマット |
| ステータスコード | 処理結果を表す数字 |
| Rustでの対応 |
|---|
| HTTPサーバー → Axum |
| JSON処理 → serde |
| 非同期 → tokio |
確認テスト
Q1. RESTful APIで「新しいユーザーを作成する」場合、適切なHTTPメソッドは?
Q2. HTTPステータスコード「404」が意味するのは?
Q3. 以下のAPIエンドポイント設計で、RESTの原則に反しているものは?
Q4. 以下のJSON文字列で構文エラーがあるのはどれ? { 'name': "Taro", "age": 25, "active": True }
Q5. ブックマーク管理APIで「特定のブックマークを更新する」場合、RESTfulなエンドポイント設計は?
次のドキュメント: 02_axum_intro.md
Axum入門
RustのモダンなWebフレームワーク「Axum」を学びます。
Axumとは
Axumは、tokioチームが開発したWebフレームワークです。
特徴
- 型安全: コンパイル時に多くのエラーを検出
- 非同期: tokioベースで高性能
- モジュラー: 必要な機能だけ使える
- エコシステム: tower/hyperとの連携
セットアップ
Cargo.toml
[dependencies]
axum = "0.7"
tokio = { version = "1", features = ["full"] }
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
Hello World
use axum::{routing::get, Router};
#[tokio::main]
async fn main() {
// ルーターを作成
let app = Router::new()
.route("/", get(hello));
// サーバーを起動
let listener = tokio::net::TcpListener::bind("0.0.0.0:3000").await.unwrap();
println!("Server running on http://localhost:3000");
axum::serve(listener, app).await.unwrap();
}
// ハンドラー関数
async fn hello() -> &'static str {
"Hello, Axum!"
}
実行:
cargo run
# 別のターミナルで
curl http://localhost:3000
# => Hello, Axum!
基本構造
use axum::{routing::get, Router};
#[tokio::main]
async fn main() {
let app = Router::new()
.route("/", get(handler1)) // GET /
.route("/users", get(handler2)) // GET /users
.route("/users", post(handler3)); // POST /users
// サーバー起動...
}
ルーティングのパターン
#![allow(unused)]
fn main() {
use axum::routing::{get, post, put, delete};
let app = Router::new()
// 基本的なルート
.route("/", get(root))
// CRUDパターン
.route("/users", get(list_users)) // GET /users
.route("/users", post(create_user)) // POST /users
.route("/users/:id", get(get_user)) // GET /users/123
.route("/users/:id", put(update_user)) // PUT /users/123
.route("/users/:id", delete(delete_user)); // DELETE /users/123
}
ハンドラー関数
基本形
#![allow(unused)]
fn main() {
async fn handler() -> impl IntoResponse {
"Hello, World!"
}
}
戻り値の種類
#![allow(unused)]
fn main() {
use axum::response::{Html, Json};
use axum::http::StatusCode;
// 文字列
async fn text() -> &'static str {
"Plain text"
}
// HTML
async fn html() -> Html<&'static str> {
Html("<h1>Hello</h1>")
}
// JSON
async fn json() -> Json<serde_json::Value> {
Json(serde_json::json!({"message": "Hello"}))
}
// ステータスコード付き
async fn with_status() -> (StatusCode, &'static str) {
(StatusCode::CREATED, "Created!")
}
// エラー
async fn not_found() -> StatusCode {
StatusCode::NOT_FOUND
}
}
JSONレスポンス
構造体をJSONで返す
#![allow(unused)]
fn main() {
use axum::Json;
use serde::Serialize;
#[derive(Serialize)]
struct User {
id: u32,
name: String,
}
async fn get_user() -> Json<User> {
let user = User {
id: 1,
name: "Taro".to_string(),
};
Json(user)
}
}
レスポンス例
{
"id": 1,
"name": "Taro"
}
パスパラメータ
URLの一部を変数として受け取ります。
#![allow(unused)]
fn main() {
use axum::extract::Path;
// /users/123 → id = 123
async fn get_user(Path(id): Path<u32>) -> String {
format!("User ID: {}", id)
}
// /users/123/posts/456 → 複数のパラメータ
async fn get_post(Path((user_id, post_id)): Path<(u32, u32)>) -> String {
format!("User: {}, Post: {}", user_id, post_id)
}
}
ルート定義
#![allow(unused)]
fn main() {
let app = Router::new()
.route("/users/:id", get(get_user))
.route("/users/:user_id/posts/:post_id", get(get_post));
}
クエリパラメータ
?key=value 形式のパラメータを受け取ります。
#![allow(unused)]
fn main() {
use axum::extract::Query;
use serde::Deserialize;
#[derive(Deserialize)]
struct Pagination {
page: Option<u32>,
limit: Option<u32>,
}
// /users?page=1&limit=10
async fn list_users(Query(params): Query<Pagination>) -> String {
let page = params.page.unwrap_or(1);
let limit = params.limit.unwrap_or(10);
format!("Page: {}, Limit: {}", page, limit)
}
}
JSONリクエストボディ
POSTやPUTでJSONデータを受け取ります。
#![allow(unused)]
fn main() {
use axum::Json;
use serde::Deserialize;
#[derive(Deserialize)]
struct CreateUser {
name: String,
email: String,
}
async fn create_user(Json(payload): Json<CreateUser>) -> String {
format!("Created user: {} ({})", payload.name, payload.email)
}
}
リクエスト例
curl -X POST http://localhost:3000/users \
-H "Content-Type: application/json" \
-d '{"name": "Taro", "email": "taro@example.com"}'
完全な例: CRUD API
use axum::{
extract::{Path, Query, State},
http::StatusCode,
routing::{get, post, put, delete},
Json, Router,
};
use serde::{Deserialize, Serialize};
use std::sync::Arc;
use tokio::sync::RwLock;
// データモデル
#[derive(Debug, Clone, Serialize, Deserialize)]
struct User {
id: u32,
name: String,
email: String,
}
#[derive(Deserialize)]
struct CreateUserRequest {
name: String,
email: String,
}
// アプリケーション状態(簡易的なインメモリDB)
type AppState = Arc<RwLock<Vec<User>>>;
#[tokio::main]
async fn main() {
let state: AppState = Arc::new(RwLock::new(vec![]));
let app = Router::new()
.route("/users", get(list_users))
.route("/users", post(create_user))
.route("/users/:id", get(get_user))
.route("/users/:id", delete(delete_user))
.with_state(state);
let listener = tokio::net::TcpListener::bind("0.0.0.0:3000").await.unwrap();
println!("Server running on http://localhost:3000");
axum::serve(listener, app).await.unwrap();
}
// ユーザー一覧
async fn list_users(State(state): State<AppState>) -> Json<Vec<User>> {
let users = state.read().await;
Json(users.clone())
}
// ユーザー作成
async fn create_user(
State(state): State<AppState>,
Json(payload): Json<CreateUserRequest>,
) -> (StatusCode, Json<User>) {
let mut users = state.write().await;
let id = users.len() as u32 + 1;
let user = User {
id,
name: payload.name,
email: payload.email,
};
users.push(user.clone());
(StatusCode::CREATED, Json(user))
}
// ユーザー取得
async fn get_user(
State(state): State<AppState>,
Path(id): Path<u32>,
) -> Result<Json<User>, StatusCode> {
let users = state.read().await;
users
.iter()
.find(|u| u.id == id)
.cloned()
.map(Json)
.ok_or(StatusCode::NOT_FOUND)
}
// ユーザー削除
async fn delete_user(
State(state): State<AppState>,
Path(id): Path<u32>,
) -> StatusCode {
let mut users = state.write().await;
let original_len = users.len();
users.retain(|u| u.id != id);
if users.len() < original_len {
StatusCode::NO_CONTENT
} else {
StatusCode::NOT_FOUND
}
}
テスト
# ユーザー作成
curl -X POST http://localhost:3000/users \
-H "Content-Type: application/json" \
-d '{"name": "Taro", "email": "taro@example.com"}'
# ユーザー一覧
curl http://localhost:3000/users
# 特定のユーザー取得
curl http://localhost:3000/users/1
# ユーザー削除
curl -X DELETE http://localhost:3000/users/1
まとめ
| 概念 | 説明 |
|---|---|
| Router | ルーティング定義 |
| Handler | リクエスト処理関数 |
| Path | URLパラメータ抽出 |
| Query | クエリパラメータ抽出 |
| Json | JSON送受信 |
| State | アプリケーション状態 |
| Extractor | 用途 |
|---|---|
Path<T> | /users/:id からIDを取得 |
Query<T> | ?page=1 からパラメータ取得 |
Json<T> | リクエストボディをパース |
State<T> | 共有状態にアクセス |
確認テスト
Q1. Axumでパスパラメータ /users/123 からIDを取得するのに使うExtractorは?
Q2. Axumで POST /users にJSONデータを受け取る場合、正しいハンドラーの引数の型は?
Q3. Query(p): Query<Params>(page: u32, limit: u32)で /users?page=2&limit=5 にアクセスしたとき、format!("{}-{}", p.page, p.limit) の出力は?
Q4. 以下のコードのエラー原因は? async fn hello() -> String { "Hello" } fn main() { let app = Router::new().route("/", get(hello)); }
Q5. GET /hello/:name で "Hello, {name}!" を返すハンドラーの正しい実装は?
次のドキュメント: 03_routing_handlers.md
ルーティングとハンドラー
Axumのルーティングとハンドラーの詳細を学びます。
ルーティングの基本
単一ルート
#![allow(unused)]
fn main() {
use axum::{routing::get, Router};
let app = Router::new()
.route("/", get(handler));
}
複数のHTTPメソッド
#![allow(unused)]
fn main() {
use axum::routing::{get, post, put, delete};
let app = Router::new()
.route("/users", get(list_users).post(create_user))
.route("/users/:id", get(get_user).put(update_user).delete(delete_user));
}
method_router
#![allow(unused)]
fn main() {
use axum::routing::MethodRouter;
// 同じパスに複数のメソッドを設定
let user_routes = get(list_users)
.post(create_user);
let app = Router::new()
.route("/users", user_routes);
}
パスパターン
静的パス
#![allow(unused)]
fn main() {
.route("/users", get(handler)) // /users
.route("/api/v1/users", get(handler)) // /api/v1/users
}
動的パス(パラメータ)
#![allow(unused)]
fn main() {
.route("/users/:id", get(handler)) // /users/123
.route("/users/:id/posts/:post_id", get(handler)) // /users/123/posts/456
}
ワイルドカード
#![allow(unused)]
fn main() {
.route("/files/*path", get(handler)) // /files/a/b/c → path = "a/b/c"
}
Extractor(データ抽出)
Path: パスパラメータ
#![allow(unused)]
fn main() {
use axum::extract::Path;
// 単一パラメータ
async fn get_user(Path(id): Path<u32>) -> String {
format!("User: {}", id)
}
// 複数パラメータ
async fn get_post(Path((user_id, post_id)): Path<(u32, u32)>) -> String {
format!("User: {}, Post: {}", user_id, post_id)
}
// 構造体で受け取る
#[derive(Deserialize)]
struct PathParams {
user_id: u32,
post_id: u32,
}
async fn get_post_struct(Path(params): Path<PathParams>) -> String {
format!("User: {}, Post: {}", params.user_id, params.post_id)
}
}
Query: クエリパラメータ
#![allow(unused)]
fn main() {
use axum::extract::Query;
use serde::Deserialize;
#[derive(Deserialize)]
struct ListParams {
page: Option<u32>,
limit: Option<u32>,
search: Option<String>,
}
// /users?page=1&limit=10&search=foo
async fn list_users(Query(params): Query<ListParams>) -> String {
let page = params.page.unwrap_or(1);
let limit = params.limit.unwrap_or(10);
format!("Page: {}, Limit: {}, Search: {:?}", page, limit, params.search)
}
}
Json: リクエストボディ
#![allow(unused)]
fn main() {
use axum::Json;
use serde::Deserialize;
#[derive(Deserialize)]
struct CreateUser {
name: String,
email: String,
#[serde(default)]
age: Option<u32>,
}
async fn create_user(Json(payload): Json<CreateUser>) -> String {
format!("Creating: {} ({})", payload.name, payload.email)
}
}
Header: ヘッダー
#![allow(unused)]
fn main() {
use axum::http::header::HeaderMap;
use axum::extract::TypedHeader;
use axum::headers::Authorization;
use axum::headers::authorization::Bearer;
// 全ヘッダー取得
async fn all_headers(headers: HeaderMap) -> String {
format!("{:?}", headers)
}
// 特定のヘッダー
async fn auth_header(
TypedHeader(auth): TypedHeader<Authorization<Bearer>>,
) -> String {
format!("Token: {}", auth.token())
}
}
State: 共有状態
#![allow(unused)]
fn main() {
use axum::extract::State;
use std::sync::Arc;
struct AppState {
db_pool: Pool,
config: Config,
}
async fn handler(State(state): State<Arc<AppState>>) -> String {
// state.db_pool や state.config にアクセス
"OK".to_string()
}
// メインでStateを設定
let state = Arc::new(AppState { ... });
let app = Router::new()
.route("/", get(handler))
.with_state(state);
}
複数のExtractor
Extractorは複数組み合わせられます。
#![allow(unused)]
fn main() {
async fn handler(
State(state): State<AppState>,
Path(id): Path<u32>,
Query(params): Query<ListParams>,
Json(body): Json<CreateUser>,
) -> impl IntoResponse {
// すべてのデータにアクセス可能
"OK"
}
}
順序の注意点
JsonやRequestなど、リクエストボディを消費するExtractorは最後に配置します。
#![allow(unused)]
fn main() {
// ✅ 正しい: Jsonは最後
async fn handler(
State(state): State<AppState>,
Path(id): Path<u32>,
Json(body): Json<Data>, // 最後
) { }
// ❌ 間違い: Jsonの後に他のExtractor
async fn handler(
Json(body): Json<Data>,
Path(id): Path<u32>, // エラー
) { }
}
レスポンス
基本的なレスポンス型
#![allow(unused)]
fn main() {
use axum::response::{Html, Json, IntoResponse};
use axum::http::StatusCode;
// 文字列
async fn text() -> &'static str {
"Hello"
}
// HTML
async fn html() -> Html<String> {
Html("<h1>Hello</h1>".to_string())
}
// JSON
async fn json() -> Json<serde_json::Value> {
Json(serde_json::json!({"status": "ok"}))
}
// ステータスコードのみ
async fn no_content() -> StatusCode {
StatusCode::NO_CONTENT
}
// ステータスコード + ボディ
async fn created() -> (StatusCode, Json<User>) {
(StatusCode::CREATED, Json(user))
}
}
カスタムレスポンス
#![allow(unused)]
fn main() {
use axum::response::{Response, IntoResponse};
use axum::http::{StatusCode, header};
async fn custom_response() -> impl IntoResponse {
(
StatusCode::OK,
[(header::CONTENT_TYPE, "text/plain")],
"Hello with custom headers"
)
}
}
Result型でエラー処理
#![allow(unused)]
fn main() {
use axum::response::IntoResponse;
use axum::http::StatusCode;
async fn get_user(Path(id): Path<u32>) -> Result<Json<User>, StatusCode> {
let user = find_user(id)
.ok_or(StatusCode::NOT_FOUND)?;
Ok(Json(user))
}
}
エラーハンドリング
カスタムエラー型
#![allow(unused)]
fn main() {
use axum::response::{IntoResponse, Response};
use axum::http::StatusCode;
use axum::Json;
// カスタムエラー型
enum AppError {
NotFound,
BadRequest(String),
Internal(String),
}
// IntoResponseを実装
impl IntoResponse for AppError {
fn into_response(self) -> Response {
let (status, message) = match self {
AppError::NotFound => (StatusCode::NOT_FOUND, "Not found"),
AppError::BadRequest(msg) => (StatusCode::BAD_REQUEST, msg.as_str()),
AppError::Internal(msg) => (StatusCode::INTERNAL_SERVER_ERROR, msg.as_str()),
};
let body = Json(serde_json::json!({
"error": message
}));
(status, body).into_response()
}
}
// ハンドラーで使用
async fn get_user(Path(id): Path<u32>) -> Result<Json<User>, AppError> {
let user = find_user(id)
.ok_or(AppError::NotFound)?;
Ok(Json(user))
}
}
anyhowとの連携
#![allow(unused)]
fn main() {
use anyhow::Result;
use axum::response::IntoResponse;
// anyhowエラーをラップ
struct AppError(anyhow::Error);
impl IntoResponse for AppError {
fn into_response(self) -> Response {
(
StatusCode::INTERNAL_SERVER_ERROR,
format!("Error: {}", self.0)
).into_response()
}
}
impl<E> From<E> for AppError
where
E: Into<anyhow::Error>,
{
fn from(err: E) -> Self {
Self(err.into())
}
}
async fn handler() -> Result<Json<Data>, AppError> {
let data = fetch_data()?; // ?でエラーを伝播
Ok(Json(data))
}
}
ルーターのネスト
nest: サブルーターを結合
#![allow(unused)]
fn main() {
// ユーザー関連のルート
fn user_routes() -> Router {
Router::new()
.route("/", get(list_users).post(create_user))
.route("/:id", get(get_user).delete(delete_user))
}
// 投稿関連のルート
fn post_routes() -> Router {
Router::new()
.route("/", get(list_posts).post(create_post))
.route("/:id", get(get_post))
}
// メインルーター
let app = Router::new()
.nest("/users", user_routes()) // /users, /users/:id
.nest("/posts", post_routes()); // /posts, /posts/:id
}
merge: ルーターを統合
#![allow(unused)]
fn main() {
let api_routes = Router::new()
.route("/users", get(list_users))
.route("/posts", get(list_posts));
let web_routes = Router::new()
.route("/", get(home))
.route("/about", get(about));
let app = api_routes.merge(web_routes);
}
ミドルウェア
基本的なミドルウェア
#![allow(unused)]
fn main() {
use axum::middleware::{self, Next};
use axum::http::Request;
use axum::response::Response;
async fn logging_middleware<B>(
request: Request<B>,
next: Next<B>,
) -> Response {
println!("Request: {} {}", request.method(), request.uri());
let response = next.run(request).await;
println!("Response: {}", response.status());
response
}
let app = Router::new()
.route("/", get(handler))
.layer(middleware::from_fn(logging_middleware));
}
tower層の使用
#![allow(unused)]
fn main() {
use tower_http::cors::CorsLayer;
use tower_http::trace::TraceLayer;
let app = Router::new()
.route("/", get(handler))
.layer(CorsLayer::permissive())
.layer(TraceLayer::new_for_http());
}
まとめ
| Extractor | 用途 |
|---|---|
Path<T> | URLパラメータ |
Query<T> | クエリパラメータ |
Json<T> | JSONボディ |
State<T> | 共有状態 |
HeaderMap | 全ヘッダー |
| Response | 用途 |
|---|---|
&str / String | テキスト |
Html<T> | HTML |
Json<T> | JSON |
StatusCode | ステータスのみ |
(StatusCode, T) | ステータス + ボディ |
確認テスト
Q1. Axumで /users?page=2&search=foo からパラメータを取得するExtractorは?
Q2. ハンドラーで複数のExtractorを使う場合、Jsonの配置について正しいのは?
Q3. Path<Params>(user_id: u32, post_id: u32)で GET /users/123/posts/456 にアクセスしたとき、format!("U:{},P:{}", p.user_id, p.post_id) の出力は?
Q4. 以下のコードのエラー原因は? async fn handler(Json(body): Json<Data>, Path(id): Path<u32>, State(state): State<AppState>)
Q5. GET /items/:id でIDが0以下なら400 Bad Request、見つからなければ404 Not Foundを返す場合、戻り値の型として適切なのは?
次のドキュメント: 04_database_sqlx.md
データベースとSQLx
RustでデータベースにアクセスするためのSQLxを学びます。
SQLxとは
SQLxは、コンパイル時にSQLクエリを検証できるRustのデータベースライブラリです。
特徴
- コンパイル時検証: SQLの誤りをコンパイル時に検出
- 非同期対応: async/awaitに対応
- マイグレーション: スキーマ管理機能内蔵
- 複数DB対応: SQLite, PostgreSQL, MySQL
このカリキュラムでは SQLite を使用します(セットアップが簡単)。
セットアップ
Cargo.toml
[dependencies]
sqlx = { version = "0.7", features = ["runtime-tokio", "sqlite"] }
tokio = { version = "1", features = ["full"] }
SQLx CLI のインストール
cargo install sqlx-cli --no-default-features --features sqlite
データベースの作成
# 環境変数を設定
export DATABASE_URL="sqlite:./database.db"
# データベースファイルを作成
sqlx database create
マイグレーション
マイグレーションファイルの作成
sqlx migrate add create_users_table
migrations/YYYYMMDDHHMMSS_create_users_table.sql が作成されます。
マイグレーションの内容
-- migrations/20240101000000_create_users_table.sql
CREATE TABLE IF NOT EXISTS users (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL,
email TEXT NOT NULL UNIQUE,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
);
マイグレーションの実行
sqlx migrate run
基本的な使い方
接続
use sqlx::sqlite::SqlitePool;
#[tokio::main]
async fn main() -> Result<(), sqlx::Error> {
let pool = SqlitePool::connect("sqlite:./database.db").await?;
// poolを使ってクエリを実行
Ok(())
}
INSERT
#![allow(unused)]
fn main() {
use sqlx::sqlite::SqlitePool;
async fn create_user(pool: &SqlitePool, name: &str, email: &str) -> Result<i64, sqlx::Error> {
let result = sqlx::query(
"INSERT INTO users (name, email) VALUES (?, ?)"
)
.bind(name)
.bind(email)
.execute(pool)
.await?;
Ok(result.last_insert_rowid())
}
}
SELECT(単一行)
#![allow(unused)]
fn main() {
use sqlx::{FromRow, sqlite::SqlitePool};
#[derive(Debug, FromRow)]
struct User {
id: i64,
name: String,
email: String,
}
async fn get_user(pool: &SqlitePool, id: i64) -> Result<Option<User>, sqlx::Error> {
let user = sqlx::query_as::<_, User>(
"SELECT id, name, email FROM users WHERE id = ?"
)
.bind(id)
.fetch_optional(pool)
.await?;
Ok(user)
}
}
SELECT(複数行)
#![allow(unused)]
fn main() {
async fn list_users(pool: &SqlitePool) -> Result<Vec<User>, sqlx::Error> {
let users = sqlx::query_as::<_, User>(
"SELECT id, name, email FROM users ORDER BY id"
)
.fetch_all(pool)
.await?;
Ok(users)
}
}
UPDATE
#![allow(unused)]
fn main() {
async fn update_user(
pool: &SqlitePool,
id: i64,
name: &str,
email: &str,
) -> Result<bool, sqlx::Error> {
let result = sqlx::query(
"UPDATE users SET name = ?, email = ? WHERE id = ?"
)
.bind(name)
.bind(email)
.bind(id)
.execute(pool)
.await?;
Ok(result.rows_affected() > 0)
}
}
DELETE
#![allow(unused)]
fn main() {
async fn delete_user(pool: &SqlitePool, id: i64) -> Result<bool, sqlx::Error> {
let result = sqlx::query("DELETE FROM users WHERE id = ?")
.bind(id)
.execute(pool)
.await?;
Ok(result.rows_affected() > 0)
}
}
query_as マクロ
構造体にマッピングする場合は query_as を使います。
#![allow(unused)]
fn main() {
use sqlx::FromRow;
#[derive(Debug, FromRow)]
struct User {
id: i64,
name: String,
email: String,
}
// query_as を使用
let users: Vec<User> = sqlx::query_as("SELECT * FROM users")
.fetch_all(&pool)
.await?;
}
コンパイル時検証
SQLxは sqlx::query! マクロでコンパイル時にSQLを検証できます。
準備(オフラインモード)
# .envファイルを作成
echo "DATABASE_URL=sqlite:./database.db" > .env
# SQLx用のメタデータを生成
cargo sqlx prepare
使用例
#![allow(unused)]
fn main() {
// コンパイル時にSQLが検証される
let user = sqlx::query!(
"SELECT id, name, email FROM users WHERE id = ?",
id
)
.fetch_optional(&pool)
.await?;
// 存在しないカラムはコンパイルエラー
// let user = sqlx::query!(
// "SELECT invalid_column FROM users" // コンパイルエラー!
// );
}
Axumとの統合
プロジェクト構造
src/
├── main.rs
├── db.rs # データベース関連
├── models.rs # データモデル
└── handlers.rs # APIハンドラー
db.rs
#![allow(unused)]
fn main() {
use sqlx::sqlite::SqlitePool;
pub async fn create_pool(database_url: &str) -> Result<SqlitePool, sqlx::Error> {
SqlitePool::connect(database_url).await
}
}
models.rs
#![allow(unused)]
fn main() {
use serde::{Deserialize, Serialize};
use sqlx::FromRow;
#[derive(Debug, Clone, Serialize, FromRow)]
pub struct User {
pub id: i64,
pub name: String,
pub email: String,
}
#[derive(Debug, Deserialize)]
pub struct CreateUser {
pub name: String,
pub email: String,
}
#[derive(Debug, Deserialize)]
pub struct UpdateUser {
pub name: Option<String>,
pub email: Option<String>,
}
}
handlers.rs
#![allow(unused)]
fn main() {
use axum::{
extract::{Path, State},
http::StatusCode,
Json,
};
use sqlx::sqlite::SqlitePool;
use crate::models::{CreateUser, UpdateUser, User};
type Pool = SqlitePool;
// ユーザー一覧
pub async fn list_users(
State(pool): State<Pool>,
) -> Result<Json<Vec<User>>, StatusCode> {
let users = sqlx::query_as::<_, User>("SELECT id, name, email FROM users")
.fetch_all(&pool)
.await
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
Ok(Json(users))
}
// ユーザー取得
pub async fn get_user(
State(pool): State<Pool>,
Path(id): Path<i64>,
) -> Result<Json<User>, StatusCode> {
let user = sqlx::query_as::<_, User>(
"SELECT id, name, email FROM users WHERE id = ?"
)
.bind(id)
.fetch_optional(&pool)
.await
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?
.ok_or(StatusCode::NOT_FOUND)?;
Ok(Json(user))
}
// ユーザー作成
pub async fn create_user(
State(pool): State<Pool>,
Json(payload): Json<CreateUser>,
) -> Result<(StatusCode, Json<User>), StatusCode> {
let result = sqlx::query(
"INSERT INTO users (name, email) VALUES (?, ?)"
)
.bind(&payload.name)
.bind(&payload.email)
.execute(&pool)
.await
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
let user = User {
id: result.last_insert_rowid(),
name: payload.name,
email: payload.email,
};
Ok((StatusCode::CREATED, Json(user)))
}
// ユーザー削除
pub async fn delete_user(
State(pool): State<Pool>,
Path(id): Path<i64>,
) -> Result<StatusCode, StatusCode> {
let result = sqlx::query("DELETE FROM users WHERE id = ?")
.bind(id)
.execute(&pool)
.await
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
if result.rows_affected() > 0 {
Ok(StatusCode::NO_CONTENT)
} else {
Err(StatusCode::NOT_FOUND)
}
}
}
main.rs
mod db;
mod handlers;
mod models;
use axum::{routing::{get, post, delete}, Router};
#[tokio::main]
async fn main() {
let database_url = std::env::var("DATABASE_URL")
.unwrap_or_else(|_| "sqlite:./database.db".to_string());
let pool = db::create_pool(&database_url)
.await
.expect("Failed to create pool");
// マイグレーションを実行
sqlx::migrate!()
.run(&pool)
.await
.expect("Failed to run migrations");
let app = Router::new()
.route("/users", get(handlers::list_users).post(handlers::create_user))
.route("/users/:id", get(handlers::get_user).delete(handlers::delete_user))
.with_state(pool);
let listener = tokio::net::TcpListener::bind("0.0.0.0:3000").await.unwrap();
println!("Server running on http://localhost:3000");
axum::serve(listener, app).await.unwrap();
}
トランザクション
#![allow(unused)]
fn main() {
use sqlx::sqlite::SqlitePool;
async fn transfer_funds(
pool: &SqlitePool,
from_id: i64,
to_id: i64,
amount: i64,
) -> Result<(), sqlx::Error> {
// トランザクション開始
let mut tx = pool.begin().await?;
// 送金元から減額
sqlx::query("UPDATE accounts SET balance = balance - ? WHERE id = ?")
.bind(amount)
.bind(from_id)
.execute(&mut *tx)
.await?;
// 送金先に加算
sqlx::query("UPDATE accounts SET balance = balance + ? WHERE id = ?")
.bind(amount)
.bind(to_id)
.execute(&mut *tx)
.await?;
// コミット
tx.commit().await?;
Ok(())
}
}
エラーハンドリング
#![allow(unused)]
fn main() {
use sqlx::Error as SqlxError;
use axum::http::StatusCode;
fn map_db_error(err: SqlxError) -> StatusCode {
match err {
SqlxError::RowNotFound => StatusCode::NOT_FOUND,
SqlxError::Database(db_err) => {
// ユニーク制約違反など
if db_err.is_unique_violation() {
StatusCode::CONFLICT
} else {
StatusCode::INTERNAL_SERVER_ERROR
}
}
_ => StatusCode::INTERNAL_SERVER_ERROR,
}
}
}
まとめ
| 操作 | メソッド |
|---|---|
| INSERT/UPDATE/DELETE | query().execute() |
| SELECT(単一) | query_as().fetch_one() / fetch_optional() |
| SELECT(複数) | query_as().fetch_all() |
| トランザクション | pool.begin() → tx.commit() |
| コマンド | 用途 |
|---|---|
sqlx database create | DB作成 |
sqlx migrate add | マイグレーション作成 |
sqlx migrate run | マイグレーション実行 |
cargo sqlx prepare | オフラインモード準備 |
確認テスト
Q1. SQLxで複数の行を取得するメソッドは?
Q2. SQLxでINSERT後に挿入されたIDを取得するには?
Q3. fetch_optional(&pool).await? でIDが存在しない場合の戻り値は?
Q4. 構造体が id, name のみで、SQLが "SELECT id, name, email FROM users" の場合の問題は?
Q5. SQLxでブックマークを削除した後、削除されたかどうかを確認する正しい方法は?
次のドキュメント: 05_authentication.md
認証
Webアプリケーションの認証の基礎を学びます。
認証とは
認証(Authentication): 「あなたは誰か」を確認すること 認可(Authorization): 「何ができるか」を確認すること
┌─────────────┐ 認証 ┌─────────────┐ 認可 ┌─────────────┐
│ ユーザー │ ───────────→ │ 本人確認 │ ───────────→ │ 権限確認 │
│ │ ID/Password │ OK/NG │ │ 許可/拒否 │
└─────────────┘ └─────────────┘ └─────────────┘
認証方式の種類
1. セッションベース認証
┌────────┐ ┌────────────┐ ┌────────┐
│ Client │ ──Login──→│ Server │ ←───────→ │ Session│
│ │ ←Cookie── │ │ │ Store │
│ │ ──Cookie─→│ 検証 │ │ │
└────────┘ └────────────┘ └────────┘
特徴:
- サーバーがセッション情報を保持
- Cookieでセッション IDを送受信
- 状態を持つ(ステートフル)
2. トークンベース認証(JWT)
┌────────┐ ┌────────────┐
│ Client │ ──Login──→│ Server │
│ │ ←JWT───── │ │
│ │ ──JWT────→│ 検証 │ ← トークン自体に情報
└────────┘ └────────────┘
特徴:
- トークン自体に情報を含む
- サーバーは状態を持たない(ステートレス)
- スケールしやすい
セッションベース認証の実装
Cargo.toml
[dependencies]
axum = "0.7"
axum-extra = { version = "0.9", features = ["cookie"] }
tokio = { version = "1", features = ["full"] }
tower-sessions = "0.12"
tower-sessions-sqlx-store = { version = "0.12", features = ["sqlite"] }
sqlx = { version = "0.7", features = ["runtime-tokio", "sqlite"] }
serde = { version = "1.0", features = ["derive"] }
uuid = { version = "1", features = ["v4"] }
argon2 = "0.5" # パスワードハッシュ
パスワードのハッシュ化
絶対にパスワードを平文で保存しない!
#![allow(unused)]
fn main() {
use argon2::{
password_hash::{rand_core::OsRng, PasswordHash, PasswordHasher, PasswordVerifier, SaltString},
Argon2,
};
// パスワードをハッシュ化
fn hash_password(password: &str) -> Result<String, argon2::password_hash::Error> {
let salt = SaltString::generate(&mut OsRng);
let argon2 = Argon2::default();
let hash = argon2.hash_password(password.as_bytes(), &salt)?;
Ok(hash.to_string())
}
// パスワードを検証
fn verify_password(password: &str, hash: &str) -> bool {
let parsed_hash = match PasswordHash::new(hash) {
Ok(h) => h,
Err(_) => return false,
};
Argon2::default()
.verify_password(password.as_bytes(), &parsed_hash)
.is_ok()
}
}
セッション管理
#![allow(unused)]
fn main() {
use axum::{
extract::State,
http::StatusCode,
response::IntoResponse,
routing::{get, post},
Json, Router,
};
use serde::{Deserialize, Serialize};
use tower_sessions::{Session, SessionManagerLayer};
use tower_sessions_sqlx_store::SqliteStore;
const USER_ID_KEY: &str = "user_id";
#[derive(Deserialize)]
struct LoginRequest {
email: String,
password: String,
}
#[derive(Serialize)]
struct UserResponse {
id: i64,
email: String,
}
// ログイン
async fn login(
session: Session,
State(pool): State<SqlitePool>,
Json(payload): Json<LoginRequest>,
) -> Result<Json<UserResponse>, StatusCode> {
// ユーザーを検索
let user = sqlx::query_as::<_, User>(
"SELECT id, email, password_hash FROM users WHERE email = ?"
)
.bind(&payload.email)
.fetch_optional(&pool)
.await
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?
.ok_or(StatusCode::UNAUTHORIZED)?;
// パスワードを検証
if !verify_password(&payload.password, &user.password_hash) {
return Err(StatusCode::UNAUTHORIZED);
}
// セッションにユーザーIDを保存
session
.insert(USER_ID_KEY, user.id)
.await
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
Ok(Json(UserResponse {
id: user.id,
email: user.email,
}))
}
// ログアウト
async fn logout(session: Session) -> StatusCode {
session.delete().await;
StatusCode::OK
}
// 現在のユーザーを取得
async fn current_user(
session: Session,
State(pool): State<SqlitePool>,
) -> Result<Json<UserResponse>, StatusCode> {
// セッションからユーザーIDを取得
let user_id: i64 = session
.get(USER_ID_KEY)
.await
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?
.ok_or(StatusCode::UNAUTHORIZED)?;
// ユーザー情報を取得
let user = sqlx::query_as::<_, User>(
"SELECT id, email FROM users WHERE id = ?"
)
.bind(user_id)
.fetch_optional(&pool)
.await
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?
.ok_or(StatusCode::NOT_FOUND)?;
Ok(Json(UserResponse {
id: user.id,
email: user.email,
}))
}
}
メイン関数
use sqlx::sqlite::SqlitePool;
use tower_sessions::SessionManagerLayer;
use tower_sessions_sqlx_store::SqliteStore;
#[tokio::main]
async fn main() {
let pool = SqlitePool::connect("sqlite:./database.db").await.unwrap();
// セッションストアを作成
let session_store = SqliteStore::new(pool.clone());
session_store.migrate().await.unwrap();
let session_layer = SessionManagerLayer::new(session_store)
.with_secure(false) // 開発環境用(本番ではtrue)
.with_http_only(true);
let app = Router::new()
.route("/login", post(login))
.route("/logout", post(logout))
.route("/me", get(current_user))
.layer(session_layer)
.with_state(pool);
let listener = tokio::net::TcpListener::bind("0.0.0.0:3000").await.unwrap();
axum::serve(listener, app).await.unwrap();
}
認証ミドルウェア
認証が必要なルートを保護するミドルウェアを作成します。
#![allow(unused)]
fn main() {
use axum::{
extract::Request,
http::StatusCode,
middleware::Next,
response::Response,
};
use tower_sessions::Session;
// 認証チェックミドルウェア
async fn require_auth(
session: Session,
request: Request,
next: Next,
) -> Result<Response, StatusCode> {
// セッションにユーザーIDがあるかチェック
let user_id: Option<i64> = session
.get(USER_ID_KEY)
.await
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
if user_id.is_none() {
return Err(StatusCode::UNAUTHORIZED);
}
// 次のハンドラーへ
Ok(next.run(request).await)
}
}
ミドルウェアの適用
#![allow(unused)]
fn main() {
use axum::middleware;
let app = Router::new()
// 認証不要なルート
.route("/login", post(login))
.route("/register", post(register))
// 認証が必要なルート
.route("/me", get(current_user))
.route("/bookmarks", get(list_bookmarks).post(create_bookmark))
.route_layer(middleware::from_fn(require_auth))
.layer(session_layer)
.with_state(pool);
}
JWT認証(参考)
JWTを使った認証の概要です。
JWTの構造
ヘッダー.ペイロード.署名
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.
eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.
SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c
Cargo.toml
[dependencies]
jsonwebtoken = "9"
JWT生成・検証
#![allow(unused)]
fn main() {
use jsonwebtoken::{encode, decode, Header, Validation, EncodingKey, DecodingKey};
use serde::{Deserialize, Serialize};
#[derive(Debug, Serialize, Deserialize)]
struct Claims {
sub: String, // ユーザーID
exp: usize, // 有効期限
}
const SECRET: &[u8] = b"your-secret-key"; // 本番では環境変数から
// JWT生成
fn create_token(user_id: i64) -> Result<String, jsonwebtoken::errors::Error> {
let expiration = chrono::Utc::now()
.checked_add_signed(chrono::Duration::hours(24))
.unwrap()
.timestamp() as usize;
let claims = Claims {
sub: user_id.to_string(),
exp: expiration,
};
encode(
&Header::default(),
&claims,
&EncodingKey::from_secret(SECRET),
)
}
// JWT検証
fn verify_token(token: &str) -> Result<Claims, jsonwebtoken::errors::Error> {
let token_data = decode::<Claims>(
token,
&DecodingKey::from_secret(SECRET),
&Validation::default(),
)?;
Ok(token_data.claims)
}
}
セキュリティのベストプラクティス
1. パスワード
- 平文保存禁止: 必ずハッシュ化(argon2, bcrypt)
- 強度要件: 最低8文字、複雑さの要件
- レート制限: ブルートフォース攻撃対策
2. セッション
- HttpOnly: JavaScriptからアクセス禁止
- Secure: HTTPS必須(本番環境)
- SameSite: CSRF対策
#![allow(unused)]
fn main() {
let session_layer = SessionManagerLayer::new(session_store)
.with_http_only(true)
.with_secure(true) // 本番環境
.with_same_site(SameSite::Strict);
}
3. 一般的な対策
- HTTPS必須(本番環境)
- CORS設定を適切に
- 入力値のバリデーション
- エラーメッセージに詳細を含めない
まとめ
| 方式 | 特徴 | 用途 |
|---|---|---|
| セッション | ステートフル、サーバー側で管理 | 従来型Webアプリ |
| JWT | ステートレス、トークンに情報含む | API、マイクロサービス |
| セキュリティ対策 |
|---|
| パスワードのハッシュ化(argon2) |
| セッションのHttpOnly/Secure設定 |
| HTTPS必須(本番) |
| 入力値のバリデーション |
確認テスト
Q1. パスワードを保存する際に正しい方法は?
Q2. セッションCookieのHttpOnly属性の目的は?
HttpOnlyはJavaScriptからCookieにアクセスできなくし、XSS攻撃によるセッションハイジャックを防ぎます。HTTPSでのみ送信するのはSecure属性です。
Q3. ログイン処理で、ユーザーが見つからない場合とパスワードが間違っている場合のレスポンスとして適切なのは?
Q4. 以下のコードの問題は?sqlx::query("INSERT INTO users (email, password) VALUES (?, ?)").bind(&email).bind(&password)
Q5. JWTとセッションベース認証の違いとして正しいのは?
次のドキュメント: 06_frontend_integration.md
フロントエンド連携
バックエンドAPIとフロントエンドの連携方法を学びます。
連携パターン
1. 静的ファイル配信 + API
┌────────────────────────────────────────────┐
│ Axumサーバー │
│ ┌──────────────┐ ┌──────────────┐ │
│ │ 静的ファイル │ │ API │ │
│ │ /static/* │ │ /api/* │ │
│ │ HTML/JS/CSS │ │ JSON │ │
│ └──────────────┘ └──────────────┘ │
└────────────────────────────────────────────┘
2. 分離デプロイ
┌──────────────┐ ┌──────────────┐
│ フロントエンド │ CORS │ APIサーバー │
│ (Vercelなど) │ ←─────→ │ (Rust) │
└──────────────┘ └──────────────┘
静的ファイルの配信
Cargo.toml
[dependencies]
tower-http = { version = "0.5", features = ["fs", "cors"] }
ディレクトリ構造
project/
├── src/
│ └── main.rs
├── static/
│ ├── index.html
│ ├── style.css
│ └── app.js
└── Cargo.toml
静的ファイルの配信設定
use axum::{routing::get, Router};
use tower_http::services::ServeDir;
#[tokio::main]
async fn main() {
let app = Router::new()
// API ルート
.route("/api/users", get(list_users))
// 静的ファイル配信
.nest_service("/", ServeDir::new("static"));
let listener = tokio::net::TcpListener::bind("0.0.0.0:3000").await.unwrap();
axum::serve(listener, app).await.unwrap();
}
index.html の例
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>My App</title>
<link rel="stylesheet" href="/style.css">
</head>
<body>
<div id="app">
<h1>ブックマーク管理</h1>
<ul id="bookmarks"></ul>
<form id="add-form">
<input type="text" name="url" placeholder="URL" required>
<input type="text" name="title" placeholder="タイトル" required>
<button type="submit">追加</button>
</form>
</div>
<script src="/app.js"></script>
</body>
</html>
JavaScript (Fetch API)
// app.js
// ブックマーク一覧を取得
async function fetchBookmarks() {
const response = await fetch('/api/bookmarks');
const bookmarks = await response.json();
const ul = document.getElementById('bookmarks');
ul.innerHTML = '';
bookmarks.forEach(bookmark => {
const li = document.createElement('li');
li.innerHTML = `
<a href="${bookmark.url}" target="_blank">${bookmark.title}</a>
<button onclick="deleteBookmark(${bookmark.id})">削除</button>
`;
ul.appendChild(li);
});
}
// ブックマークを追加
async function addBookmark(url, title) {
const response = await fetch('/api/bookmarks', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ url, title }),
});
if (response.ok) {
fetchBookmarks(); // 一覧を更新
}
}
// ブックマークを削除
async function deleteBookmark(id) {
const response = await fetch(`/api/bookmarks/${id}`, {
method: 'DELETE',
});
if (response.ok) {
fetchBookmarks(); // 一覧を更新
}
}
// フォーム送信
document.getElementById('add-form').addEventListener('submit', async (e) => {
e.preventDefault();
const formData = new FormData(e.target);
await addBookmark(formData.get('url'), formData.get('title'));
e.target.reset();
});
// 初期読み込み
fetchBookmarks();
CORS(Cross-Origin Resource Sharing)
フロントエンドとバックエンドが異なるオリジンの場合、CORSの設定が必要です。
なぜCORSが必要か
フロントエンド: https://frontend.example.com
バックエンド: https://api.example.com
→ オリジンが異なるため、ブラウザがリクエストをブロック
→ CORS設定で許可する
CORS設定
use axum::{routing::get, Router};
use tower_http::cors::{CorsLayer, Any};
use http::Method;
#[tokio::main]
async fn main() {
// CORS設定
let cors = CorsLayer::new()
.allow_origin(Any) // すべてのオリジンを許可(開発用)
.allow_methods([Method::GET, Method::POST, Method::PUT, Method::DELETE])
.allow_headers(Any);
let app = Router::new()
.route("/api/bookmarks", get(list_bookmarks).post(create_bookmark))
.layer(cors);
// ...
}
本番環境のCORS設定
#![allow(unused)]
fn main() {
use tower_http::cors::{CorsLayer, AllowOrigin};
use http::{Method, HeaderValue};
let cors = CorsLayer::new()
.allow_origin(AllowOrigin::exact(
"https://your-frontend.com".parse().unwrap()
))
.allow_methods([Method::GET, Method::POST, Method::PUT, Method::DELETE])
.allow_headers([
http::header::CONTENT_TYPE,
http::header::AUTHORIZATION,
])
.allow_credentials(true); // Cookieを使う場合
}
htmx によるシンプルな連携
htmxはJavaScriptをほぼ書かずにAjaxを実現できるライブラリです。
htmxの基本
<!DOCTYPE html>
<html lang="ja">
<head>
<script src="https://unpkg.com/htmx.org@1.9.10"></script>
</head>
<body>
<!-- クリックでGETリクエスト -->
<button hx-get="/api/hello" hx-target="#result">
挨拶を取得
</button>
<!-- 結果を表示 -->
<div id="result"></div>
</body>
</html>
htmxの主な属性
| 属性 | 説明 |
|---|---|
hx-get | GETリクエスト |
hx-post | POSTリクエスト |
hx-put | PUTリクエスト |
hx-delete | DELETEリクエスト |
hx-target | 結果を挿入する要素 |
hx-swap | 挿入方法(innerHTML, outerHTML等) |
hx-trigger | トリガーイベント |
htmxでのCRUD例
<!-- ブックマーク一覧 -->
<div id="bookmarks" hx-get="/api/bookmarks" hx-trigger="load">
<!-- サーバーからHTMLが挿入される -->
</div>
<!-- 追加フォーム -->
<form hx-post="/api/bookmarks"
hx-target="#bookmarks"
hx-swap="beforeend">
<input type="text" name="url" placeholder="URL" required>
<input type="text" name="title" placeholder="タイトル" required>
<button type="submit">追加</button>
</form>
サーバー側(HTMLを返す)
#![allow(unused)]
fn main() {
use axum::response::Html;
// htmx用:HTMLフラグメントを返す
async fn list_bookmarks_html(State(pool): State<SqlitePool>) -> Html<String> {
let bookmarks = sqlx::query_as::<_, Bookmark>("SELECT * FROM bookmarks")
.fetch_all(&pool)
.await
.unwrap_or_default();
let html = bookmarks
.iter()
.map(|b| format!(
r#"<li>
<a href="{}">{}</a>
<button hx-delete="/api/bookmarks/{}"
hx-target="closest li"
hx-swap="outerHTML">削除</button>
</li>"#,
b.url, b.title, b.id
))
.collect::<Vec<_>>()
.join("\n");
Html(html)
}
// 追加後は新しいリストアイテムを返す
async fn create_bookmark_html(
State(pool): State<SqlitePool>,
Form(payload): Form<CreateBookmark>,
) -> Html<String> {
// ... DBに保存 ...
Html(format!(
r#"<li>
<a href="{}">{}</a>
<button hx-delete="/api/bookmarks/{}"
hx-target="closest li"
hx-swap="outerHTML">削除</button>
</li>"#,
payload.url, payload.title, id
))
}
}
環境変数による設定
.env ファイル
DATABASE_URL=sqlite:./database.db
FRONTEND_URL=http://localhost:5173
環境変数の読み込み
use std::env;
fn main() {
// .envファイルを読み込み(開発用)
dotenv::dotenv().ok();
let database_url = env::var("DATABASE_URL")
.expect("DATABASE_URL must be set");
let frontend_url = env::var("FRONTEND_URL")
.unwrap_or_else(|_| "http://localhost:3000".to_string());
// ...
}
APIレスポンスの形式
成功時
{
"data": {
"id": 1,
"url": "https://example.com",
"title": "Example"
}
}
エラー時
{
"error": {
"code": "NOT_FOUND",
"message": "Bookmark not found"
}
}
統一レスポンス型
#![allow(unused)]
fn main() {
use serde::Serialize;
#[derive(Serialize)]
#[serde(untagged)]
enum ApiResponse<T> {
Success { data: T },
Error { error: ApiError },
}
#[derive(Serialize)]
struct ApiError {
code: String,
message: String,
}
// 使用例
async fn get_bookmark(Path(id): Path<i64>) -> Json<ApiResponse<Bookmark>> {
match find_bookmark(id).await {
Ok(bookmark) => Json(ApiResponse::Success { data: bookmark }),
Err(_) => Json(ApiResponse::Error {
error: ApiError {
code: "NOT_FOUND".to_string(),
message: "Bookmark not found".to_string(),
}
}),
}
}
}
まとめ
| 方式 | 特徴 | 用途 |
|---|---|---|
| 静的ファイル配信 | シンプル、同一オリジン | 小〜中規模アプリ |
| 分離デプロイ | 柔軟、スケーラブル | 大規模アプリ |
| htmx | JavaScript最小限 | シンプルなUI |
| 設定 | 用途 |
|---|---|
| ServeDir | 静的ファイル配信 |
| CorsLayer | CORS設定 |
| 環境変数 | 設定の外部化 |
確認テスト
Q1. CORSが必要になるのはどのような場合?
Q2. Fetch APIで JSONを送信する際に必要なヘッダーは?
Content-Type: application/json ヘッダーが必要です。サーバーにボディの形式を伝えます。
Q3. htmxのhx-swap="outerHTML"の動作は?
outerHTMLはターゲット要素自体を含めて置き換えます。innerHTMLは要素の中身だけを置き換えます。
Q4. fetch APIでJSON送信時にContent-Typeヘッダーがない場合の問題は?
Q5. allow_credentials(true)を使う場合のCORS設定で正しいものは?
allow_credentials(true)を使う場合、セキュリティ上の理由からallow_origin(Any)は使えません。特定のオリジンを明示的に指定する必要があります。
次のドキュメント: 07_deployment.md
デプロイメント
Rustアプリケーションのビルドとデプロイ方法を学びます。
リリースビルド
開発ビルド vs リリースビルド
# 開発ビルド(デバッグ情報あり、最適化なし)
cargo build
# リリースビルド(最適化あり、高速)
cargo build --release
| 開発ビルド | リリースビルド | |
|---|---|---|
| コマンド | cargo build | cargo build --release |
| 出力先 | target/debug/ | target/release/ |
| 最適化 | なし | あり |
| ビルド時間 | 短い | 長い |
| 実行速度 | 遅い | 速い |
| バイナリサイズ | 大きい | 小さい |
実行
# 開発
cargo run
# リリース
cargo run --release
# または直接バイナリを実行
./target/release/my-app
バイナリの最適化
Cargo.toml の設定
[profile.release]
# 最適化レベル(0-3、sはサイズ優先、zは最小サイズ)
opt-level = 3
# リンク時最適化(LTO)
lto = true
# コード生成ユニット(1が最も最適化される)
codegen-units = 1
# パニック時の動作(abort = バイナリ小さくなる)
panic = "abort"
# デバッグシンボルを削除
strip = true
バイナリサイズの確認
# サイズ確認
ls -lh target/release/my-app
# さらに小さくする(stripコマンド)
strip target/release/my-app
環境変数と設定
環境変数の読み込み
#![allow(unused)]
fn main() {
use std::env;
struct Config {
database_url: String,
port: u16,
environment: String,
}
impl Config {
fn from_env() -> Self {
Self {
database_url: env::var("DATABASE_URL")
.expect("DATABASE_URL must be set"),
port: env::var("PORT")
.unwrap_or_else(|_| "3000".to_string())
.parse()
.expect("PORT must be a number"),
environment: env::var("RUST_ENV")
.unwrap_or_else(|_| "development".to_string()),
}
}
}
}
.env ファイル(開発用)
# .env
DATABASE_URL=sqlite:./database.db
PORT=3000
RUST_ENV=development
// dotenvy クレートを使用
fn main() {
dotenvy::dotenv().ok(); // .envファイルを読み込み
let config = Config::from_env();
// ...
}
Docker
Dockerfile(マルチステージビルド)
# ビルドステージ
FROM rust:1.75 AS builder
WORKDIR /app
# 依存関係をキャッシュするため、先にCargo.tomlだけコピー
COPY Cargo.toml Cargo.lock ./
RUN mkdir src && echo "fn main() {}" > src/main.rs
RUN cargo build --release
RUN rm -rf src
# ソースコードをコピーしてビルド
COPY . .
RUN touch src/main.rs # タイムスタンプ更新
RUN cargo build --release
# 実行ステージ
FROM debian:bookworm-slim
# 必要なライブラリをインストール
RUN apt-get update && apt-get install -y \
ca-certificates \
libssl3 \
&& rm -rf /var/lib/apt/lists/*
# 非rootユーザーで実行
RUN useradd -m appuser
USER appuser
WORKDIR /app
# ビルド成果物をコピー
COPY --from=builder /app/target/release/my-app /app/my-app
# 静的ファイルをコピー(必要な場合)
COPY --from=builder /app/static /app/static
ENV PORT=3000
EXPOSE 3000
CMD ["./my-app"]
.dockerignore
target/
.git/
.env
*.md
Dockerfile
.dockerignore
Dockerコマンド
# イメージのビルド
docker build -t my-app .
# コンテナの実行
docker run -p 3000:3000 \
-e DATABASE_URL=sqlite:/app/data/database.db \
-v $(pwd)/data:/app/data \
my-app
# バックグラウンドで実行
docker run -d -p 3000:3000 --name my-app my-app
Docker Compose
docker-compose.yml
version: '3.8'
services:
app:
build: .
ports:
- "3000:3000"
environment:
- DATABASE_URL=sqlite:/app/data/database.db
- RUST_ENV=production
volumes:
- ./data:/app/data
restart: unless-stopped
# PostgreSQLを使う場合
# db:
# image: postgres:15
# environment:
# POSTGRES_USER: user
# POSTGRES_PASSWORD: password
# POSTGRES_DB: myapp
# volumes:
# - postgres_data:/var/lib/postgresql/data
# volumes:
# postgres_data:
Docker Composeコマンド
# 起動
docker-compose up
# バックグラウンドで起動
docker-compose up -d
# 停止
docker-compose down
# ログ確認
docker-compose logs -f app
# 再ビルド
docker-compose up --build
クラウドデプロイ
Railway
# Railway CLIをインストール
npm install -g @railway/cli
# ログイン
railway login
# プロジェクト作成
railway init
# デプロイ
railway up
Fly.io
# Fly CLIをインストール
curl -L https://fly.io/install.sh | sh
# ログイン
fly auth login
# アプリ作成
fly launch
# デプロイ
fly deploy
fly.toml
app = "my-app"
primary_region = "nrt" # 東京
[build]
[http_service]
internal_port = 3000
force_https = true
[env]
RUST_ENV = "production"
Shuttle(Rust専用)
# Shuttle CLIをインストール
cargo install cargo-shuttle
# プロジェクト作成
cargo shuttle init
# ローカル実行
cargo shuttle run
# デプロイ
cargo shuttle deploy
ヘルスチェック
ヘルスチェックエンドポイント
#![allow(unused)]
fn main() {
async fn health_check() -> &'static str {
"OK"
}
// より詳細なチェック
async fn health_detailed(State(pool): State<SqlitePool>) -> Json<HealthStatus> {
let db_ok = sqlx::query("SELECT 1")
.execute(&pool)
.await
.is_ok();
Json(HealthStatus {
status: if db_ok { "healthy" } else { "unhealthy" },
database: db_ok,
version: env!("CARGO_PKG_VERSION"),
})
}
#[derive(Serialize)]
struct HealthStatus {
status: &'static str,
database: bool,
version: &'static str,
}
}
ルートに追加
#![allow(unused)]
fn main() {
let app = Router::new()
.route("/health", get(health_check))
.route("/health/detailed", get(health_detailed))
// ... 他のルート
}
ログ設定
tracing クレート
[dependencies]
tracing = "0.1"
tracing-subscriber = { version = "0.3", features = ["env-filter"] }
use tracing::{info, warn, error};
use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt};
fn init_logging() {
tracing_subscriber::registry()
.with(
tracing_subscriber::EnvFilter::try_from_default_env()
.unwrap_or_else(|_| "my_app=debug,tower_http=debug".into()),
)
.with(tracing_subscriber::fmt::layer())
.init();
}
#[tokio::main]
async fn main() {
init_logging();
info!("Starting server...");
// ...
}
リクエストログ
#![allow(unused)]
fn main() {
use tower_http::trace::TraceLayer;
let app = Router::new()
.route("/", get(handler))
.layer(TraceLayer::new_for_http());
}
デプロイチェックリスト
セキュリティ
- 環境変数でシークレットを管理
- HTTPSを強制(本番)
- CORSを適切に設定
- SQLインジェクション対策(パラメータバインド)
パフォーマンス
- リリースビルドを使用
- データベース接続プール設定
- 適切なタイムアウト設定
運用
- ヘルスチェックエンドポイント
- ログ出力の設定
- エラーハンドリング
- グレースフルシャットダウン
グレースフルシャットダウン
use tokio::signal;
#[tokio::main]
async fn main() {
let app = Router::new()
// ... routes ...
;
let listener = tokio::net::TcpListener::bind("0.0.0.0:3000").await.unwrap();
axum::serve(listener, app)
.with_graceful_shutdown(shutdown_signal())
.await
.unwrap();
}
async fn shutdown_signal() {
let ctrl_c = async {
signal::ctrl_c()
.await
.expect("Failed to install Ctrl+C handler");
};
#[cfg(unix)]
let terminate = async {
signal::unix::signal(signal::unix::SignalKind::terminate())
.expect("Failed to install signal handler")
.recv()
.await;
};
#[cfg(not(unix))]
let terminate = std::future::pending::<()>();
tokio::select! {
_ = ctrl_c => {},
_ = terminate => {},
}
println!("Shutting down gracefully...");
}
まとめ
| コマンド | 用途 |
|---|---|
cargo build --release | リリースビルド |
docker build | Dockerイメージ作成 |
docker-compose up | Docker Compose起動 |
| デプロイ先 | 特徴 |
|---|---|
| Railway | シンプル、自動デプロイ |
| Fly.io | グローバル、高速 |
| Shuttle | Rust専用、簡単 |
確認テスト
Q1. cargo build --release の特徴として正しいものは?
Q2. Dockerのマルチステージビルドを使う主な理由は?
Q3. マルチステージビルドなしのDockerfileの主な問題点は?
Q4. std::env::var("PORT")の戻り値の型は?
env::varはResult<String, VarError>を返します。環境変数が存在しない場合はErrになるため、適切に処理する必要があります。
Q5. ヘルスチェックでデータベース接続失敗時に返すべきHTTPステータスコードは?
Phase 5 完了!
おめでとうございます!Phase 5を完了しました。
学んだこと:
- HTTP/RESTの基礎
- Axumによるウェブアプリケーション開発
- SQLxによるデータベース操作
- 認証の実装
- フロントエンド連携
- デプロイメント
次は最終プロジェクトに挑戦しましょう!
次のドキュメント: 最終プロジェクト
よくあるエラーと対処法
Rust学習中に遭遇しやすいエラーとその解決方法をまとめました。
所有権関連のエラー
E0382: value borrowed after move
エラーメッセージ
error[E0382]: borrow of moved value: `s`
--> src/main.rs:4:20
|
2 | let s = String::from("hello");
| - move occurs because `s` has type `String`
3 | let s2 = s;
| - value moved here
4 | println!("{}", s);
| ^ value borrowed here after move
原因: 値が別の変数にムーブされた後に使用しようとしている
解決方法:
#![allow(unused)]
fn main() {
// 方法1: クローンする
let s = String::from("hello");
let s2 = s.clone();
println!("{}", s); // OK
// 方法2: 参照を使う
let s = String::from("hello");
let s2 = &s;
println!("{}", s); // OK
}
E0502: cannot borrow as mutable because it is also borrowed as immutable
エラーメッセージ
error[E0502]: cannot borrow `v` as mutable because it is also borrowed as immutable
--> src/main.rs:4:5
|
3 | let first = &v[0];
| - immutable borrow occurs here
4 | v.push(4);
| ^^^^^^^^^ mutable borrow occurs here
5 | println!("{}", first);
| ----- immutable borrow later used here
原因: 不変参照がある間に可変操作をしようとしている
解決方法:
#![allow(unused)]
fn main() {
// 方法1: 不変参照を先に使い切る
let mut v = vec![1, 2, 3];
let first = &v[0];
println!("{}", first); // ここで使い切る
v.push(4); // その後で変更
// 方法2: インデックスを保存する
let mut v = vec![1, 2, 3];
let first_idx = 0;
v.push(4);
println!("{}", v[first_idx]);
}
E0499: cannot borrow as mutable more than once
エラーメッセージ
error[E0499]: cannot borrow `x` as mutable more than once at a time
--> src/main.rs:4:17
|
3 | let r1 = &mut x;
| ------ first mutable borrow occurs here
4 | let r2 = &mut x;
| ^^^^^^ second mutable borrow occurs here
5 | println!("{}, {}", r1, r2);
| -- first borrow later used here
原因: 可変参照を同時に複数作ろうとしている
解決方法:
#![allow(unused)]
fn main() {
// 方法1: 順番に使う
let mut x = 5;
let r1 = &mut x;
*r1 += 1;
// r1のスコープ終了
let r2 = &mut x;
*r2 += 1;
// 方法2: ブロックでスコープを分ける
let mut x = 5;
{
let r1 = &mut x;
*r1 += 1;
}
{
let r2 = &mut x;
*r2 += 1;
}
}
ライフタイム関連のエラー
E0106: missing lifetime specifier
エラーメッセージ
error[E0106]: missing lifetime specifier
--> src/main.rs:1:33
|
1 | fn longest(x: &str, y: &str) -> &str {
| ---- ---- ^ expected named lifetime parameter
原因: 複数の参照を受け取って参照を返す関数でライフタイムが不明
解決方法:
#![allow(unused)]
fn main() {
// ライフタイム注釈を追加
fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
if x.len() > y.len() {
x
} else {
y
}
}
}
E0597: borrowed value does not live long enough
エラーメッセージ
error[E0597]: `x` does not live long enough
--> src/main.rs:4:9
|
3 | let r;
| - borrow later stored here
4 | let x = 5;
5 | r = &x;
| ^^ borrowed value does not live long enough
6 | }
| - `x` dropped here while still borrowed
原因: 参照先がスコープを抜けて無効になる
解決方法:
#![allow(unused)]
fn main() {
// 参照先を外側のスコープに移動
let x = 5;
let r = &x;
println!("{}", r);
}
型関連のエラー
E0308: mismatched types
エラーメッセージ
error[E0308]: mismatched types
--> src/main.rs:2:18
|
2 | let x: i32 = "hello";
| --- ^^^^^^^ expected `i32`, found `&str`
| |
| expected due to this
原因: 期待される型と実際の型が一致しない
解決方法:
#![allow(unused)]
fn main() {
// 正しい型を使う
let x: i32 = 42;
// または型注釈を省略
let x = "hello";
}
E0277: the trait bound is not satisfied
エラーメッセージ
error[E0277]: the trait bound `MyStruct: std::fmt::Debug` is not satisfied
--> src/main.rs:5:22
|
5 | println!("{:?}", my_struct);
| ^^^^^^^^^ `MyStruct` cannot be formatted using `{:?}`
原因: 必要なトレイトが実装されていない
解決方法:
// deriveでトレイトを実装
#[derive(Debug)]
struct MyStruct {
value: i32,
}
fn main() {
let my_struct = MyStruct { value: 42 };
println!("{:?}", my_struct); // OK
}
Option/Result関連のエラー
cannot use ? in a function that returns ()
エラーメッセージ
error[E0277]: the `?` operator can only be used in a function that returns `Result` or `Option`
--> src/main.rs:3:13
|
3 | let f = File::open("file.txt")?;
| ^^^^^^^^^^^^^^^^^^^^^^^ cannot use the `?` operator in a function that returns `()`
原因: ?演算子はResultかOptionを返す関数でしか使えない
解決方法:
// 方法1: 関数の戻り値をResultにする
fn main() -> Result<(), std::io::Error> {
let f = File::open("file.txt")?;
Ok(())
}
// 方法2: matchでハンドリング
fn main() {
match File::open("file.txt") {
Ok(f) => { /* 処理 */ },
Err(e) => eprintln!("Error: {}", e),
}
}
// 方法3: unwrap(エラー時はパニック)
fn main() {
let f = File::open("file.txt").unwrap();
}
method unwrap not found
エラーメッセージ
error[E0599]: no method named `unwrap` found for type `i32`
原因: unwrapはOptionやResultのメソッドで、普通の値には使えない
解決方法:
#![allow(unused)]
fn main() {
// Optionを返す関数の結果に使う
let v = vec![1, 2, 3];
let first = v.get(0).unwrap(); // get()はOption<&T>を返す
}
モジュール関連のエラー
E0432: unresolved import
エラーメッセージ
error[E0432]: unresolved import `crate::models`
--> src/main.rs:1:5
|
1 | use crate::models;
| ^^^^^^^^^^^^^ no `models` in the root
原因: モジュールが正しく宣言されていない
解決方法:
#![allow(unused)]
fn main() {
// main.rs または lib.rs でモジュールを宣言
mod models; // これを追加
use crate::models::User;
}
ファイル構造:
src/
├── main.rs # mod models; を宣言
└── models.rs # または models/mod.rs
E0603: function is private
エラーメッセージ
error[E0603]: function `internal_function` is private
--> src/main.rs:3:14
|
3 | my_module::internal_function();
| ^^^^^^^^^^^^^^^^^ private function
原因: 非公開の関数にアクセスしようとしている
解決方法:
#![allow(unused)]
fn main() {
// 関数をpubにする
pub fn internal_function() {
// ...
}
}
非同期関連のエラー
future cannot be sent between threads safely
エラーメッセージ
error: future cannot be sent between threads safely
原因: 非同期タスク内でSendでない値を使っている
解決方法:
#![allow(unused)]
fn main() {
// 方法1: Arcでラップ
use std::sync::Arc;
let data = Arc::new(data);
// 方法2: 値をクローンしてタスクに渡す
let data = data.clone();
tokio::spawn(async move {
// data を使用
});
}
async block/function expected but found fn
エラーメッセージ
error: `await` is only allowed inside `async` functions and blocks
原因: asyncでない関数内でawaitを使っている
解決方法:
#![allow(unused)]
fn main() {
// 関数をasyncにする
async fn my_function() {
let result = some_async_fn().await;
}
}
コンパイル時のヒント
未使用の変数警告
warning: unused variable: `x`
--> src/main.rs:2:9
|
2 | let x = 5;
| ^ help: if this is intentional, prefix it with an underscore: `_x`
解決方法:
#![allow(unused)]
fn main() {
// 使う予定がない場合はアンダースコアを付ける
let _x = 5;
// または完全に無視
let _ = some_function();
}
デバッグのコツ
1. 型を確認する
#![allow(unused)]
fn main() {
// コンパイラに型を教えてもらう
let x: () = some_expression; // エラーメッセージで型がわかる
}
2. 中間値を出力する
#![allow(unused)]
fn main() {
// dbg!マクロを使う
let result = dbg!(some_function());
// 出力: [src/main.rs:2] some_function() = 42
}
3. コンパイラの提案を読む
help: consider borrowing here: `&v`
Rustコンパイラは多くの場合、修正方法を提案してくれます。help:の行を注意深く読みましょう。
まとめ
| エラー種別 | 主な原因 | 対処法 |
|---|---|---|
| E0382 | ムーブ後の使用 | clone()または参照を使う |
| E0502 | 借用ルール違反 | 参照の使用順序を変える |
| E0106 | ライフタイム不明 | ライフタイム注釈を追加 |
| E0308 | 型の不一致 | 正しい型を使う |
| E0277 | トレイト未実装 | deriveまたは手動実装 |
エラーメッセージを落ち着いて読み、コンパイラの提案に従うことが大切です。
文字列型変換ガイド
Rustの文字列型の変換方法をまとめました。
文字列型の種類
| 型 | 説明 | 所有権 |
|---|---|---|
String | ヒープ上の可変文字列 | あり |
&str | 文字列スライス(参照) | なし |
&String | Stringへの参照 | なし |
&'static str | 静的な文字列リテラル | なし |
変換早見表
.to_string()
┌────────────────────┐
│ ▼
&str ◄──────────────── String
│ &s, .as_str() │
│ │
│ │
.to_owned() .clone()
│ │
▼ ▼
String String
&str → String
#![allow(unused)]
fn main() {
// 方法1: to_string()
let s: &str = "hello";
let string: String = s.to_string();
// 方法2: to_owned()
let string: String = s.to_owned();
// 方法3: String::from()
let string: String = String::from(s);
// 方法4: into()
let string: String = s.into();
}
どれを使うべき?
to_string(): 最も一般的to_owned(): 「所有権を得る」という意図が明確String::from(): 型変換であることが明確into(): ジェネリクスで便利
String → &str
#![allow(unused)]
fn main() {
let s: String = String::from("hello");
// 方法1: &演算子(自動deref)
let slice: &str = &s;
// 方法2: as_str()
let slice: &str = s.as_str();
// 方法3: スライス構文
let slice: &str = &s[..];
}
どれを使うべき?
&s: 最もシンプルas_str(): 意図が明確
数値 → String
#![allow(unused)]
fn main() {
// 方法1: to_string()
let n: i32 = 42;
let s: String = n.to_string();
// 方法2: format!マクロ
let s: String = format!("{}", n);
// フォーマット指定
let hex: String = format!("{:x}", 255); // "ff"
let padded: String = format!("{:05}", 42); // "00042"
}
String → 数値
#![allow(unused)]
fn main() {
let s: &str = "42";
// 方法1: parse()
let n: i32 = s.parse().unwrap();
// 方法2: parse()(型を明示)
let n = s.parse::<i32>().unwrap();
// エラーハンドリング
match s.parse::<i32>() {
Ok(n) => println!("数値: {}", n),
Err(e) => println!("パースエラー: {}", e),
}
}
char → String
#![allow(unused)]
fn main() {
let c: char = 'A';
// 方法1: to_string()
let s: String = c.to_string();
// 方法2: String::from()
let s: String = String::from(c);
// 方法3: format!
let s: String = format!("{}", c);
}
String → char
#![allow(unused)]
fn main() {
let s: String = String::from("A");
// 最初の文字を取得
let c: char = s.chars().next().unwrap();
// すべての文字をイテレート
for c in s.chars() {
println!("{}", c);
}
}
Vec ↔ String
#![allow(unused)]
fn main() {
// String → Vec<u8>
let s = String::from("hello");
let bytes: Vec<u8> = s.into_bytes();
// Vec<u8> → String(UTF-8として解釈)
let bytes = vec![104, 101, 108, 108, 111]; // "hello"
let s: String = String::from_utf8(bytes).unwrap();
// 失敗する可能性がある場合
match String::from_utf8(bytes) {
Ok(s) => println!("{}", s),
Err(e) => println!("Invalid UTF-8: {}", e),
}
// lossy変換(無効なバイトは置換)
let s = String::from_utf8_lossy(&bytes);
}
&[u8] → &str
#![allow(unused)]
fn main() {
let bytes: &[u8] = b"hello";
// UTF-8として解釈
let s: &str = std::str::from_utf8(bytes).unwrap();
// または
match std::str::from_utf8(bytes) {
Ok(s) => println!("{}", s),
Err(e) => println!("Invalid UTF-8: {}", e),
}
}
PathBuf ↔ String
#![allow(unused)]
fn main() {
use std::path::PathBuf;
// String → PathBuf
let s = String::from("/path/to/file");
let path = PathBuf::from(&s);
// PathBuf → String
let path = PathBuf::from("/path/to/file");
let s: String = path.to_string_lossy().into_owned();
// PathBuf → &str(失敗する可能性あり)
if let Some(s) = path.to_str() {
println!("{}", s);
}
}
OsString ↔ String
#![allow(unused)]
fn main() {
use std::ffi::OsString;
// String → OsString
let s = String::from("hello");
let os_string = OsString::from(&s);
// OsString → String
let os_string = OsString::from("hello");
match os_string.into_string() {
Ok(s) => println!("{}", s),
Err(os_string) => println!("Invalid UTF-8"),
}
// lossy変換
let s = os_string.to_string_lossy().into_owned();
}
CString ↔ String(FFI用)
#![allow(unused)]
fn main() {
use std::ffi::{CString, CStr};
// String → CString
let s = String::from("hello");
let c_string = CString::new(s).unwrap(); // NULLバイトがあるとエラー
// CString → String
let c_string = CString::new("hello").unwrap();
let s: String = c_string.into_string().unwrap();
// &CStr → &str
let c_str: &CStr = c_string.as_c_str();
let s: &str = c_str.to_str().unwrap();
}
よくある変換パターン
関数の引数として
#![allow(unused)]
fn main() {
// &strを受け取る(推奨)
fn process(s: &str) {
// String も &str も渡せる
}
// 使用
process("literal"); // &str
process(&String::from("s")); // &String → &str(自動deref)
process(String::from("s").as_str()); // 明示的
}
構造体のフィールドとして
#![allow(unused)]
fn main() {
// 所有権が必要な場合
struct User {
name: String, // 所有する
}
// 参照でよい場合(ライフタイム必要)
struct UserRef<'a> {
name: &'a str,
}
}
戻り値として
#![allow(unused)]
fn main() {
// 新しい文字列を作る場合
fn create_greeting(name: &str) -> String {
format!("Hello, {}!", name)
}
// 入力の一部を返す場合(ライフタイム必要)
fn first_word(s: &str) -> &str {
&s[..s.find(' ').unwrap_or(s.len())]
}
}
まとめ
| 変換 | 方法 |
|---|---|
&str → String | .to_string(), .to_owned() |
String → &str | &s, .as_str() |
数値 → String | .to_string(), format!() |
String → 数値 | .parse() |
Vec<u8> → String | String::from_utf8() |
String → Vec<u8> | .into_bytes() |
基本方針:
- 関数の引数は
&strで受け取る(柔軟性) - 所有権が必要なら
Stringを使う - 変換が必要な時は目的に合った方法を選ぶ
エラーメッセージの読み方
Rustコンパイラのエラーメッセージを正しく読み解く方法を学びます。
エラーメッセージの構造
error[E0382]: borrow of moved value: `s`
--> src/main.rs:4:20
|
2 | let s = String::from("hello");
| - move occurs because `s` has type `String`, which does not implement the `Copy` trait
3 | let s2 = s;
| - value moved here
4 | println!("{}", s);
| ^ value borrowed here after move
|
help: consider cloning the value if the performance cost is acceptable
|
3 | let s2 = s.clone();
| ++++++++
各部分の説明
-
エラーコード:
error[E0382]E0382は一意のエラーコードrustc --explain E0382で詳細な説明が見られる
-
エラーの要約:
borrow of moved value: 's'- 何が問題かの簡潔な説明
-
ファイル位置:
--> src/main.rs:4:20- ファイル名:行番号:列番号
-
コードスニペット: エラー箇所のコードと注釈
|の後にコードが表示される^や-でエラー箇所を指示- 追加の説明が付く
-
help: 修正方法の提案
- 具体的なコード修正が提案される
エラーレベル
error(エラー)
error[E0382]: ...
- コンパイルが失敗する
- 必ず修正が必要
warning(警告)
warning: unused variable: `x`
- コンパイルは成功する
- 潜在的な問題を示唆
- 修正することを推奨
note(注記)
note: required by a bound in `HashMap::insert`
- 追加の情報を提供
- エラーの原因を理解するのに役立つ
help(ヘルプ)
help: consider borrowing here: `&x`
- 具体的な修正案
- 積極的に参考にしよう
読み方のコツ
1. エラーコードを確認する
# 詳細な説明を見る
rustc --explain E0382
これにより、エラーの詳細な説明と例が表示されます。
2. 最初のエラーから修正する
error[E0382]: ...
error[E0599]: ...
error[E0308]: ...
複数のエラーがある場合、最初のエラーを修正すると他のエラーも消えることが多い。
3. 注釈を丁寧に読む
2 | let s = String::from("hello");
| - move occurs because `s` has type `String`
↑ この説明が重要
- や ^ の横の説明文が問題の原因を教えてくれます。
4. helpを活用する
help: consider cloning the value
|
3 | let s2 = s.clone();
| ++++++++
+ で囲まれた部分が追加すべきコードです。
実践例
例1: 型の不一致
error[E0308]: mismatched types
--> src/main.rs:2:18
|
2 | let x: i32 = "hello";
| --- ^^^^^^^ expected `i32`, found `&str`
| |
| expected due to this
読み方:
mismatched types: 型が一致しないexpected i32, found &str:i32を期待したが&strがあったexpected due to this: 型注釈i32が原因
修正:
#![allow(unused)]
fn main() {
let x: i32 = 42; // i32にする
// または
let x: &str = "hello"; // &strにする
}
例2: 借用ルール違反
error[E0502]: cannot borrow `v` as mutable because it is also borrowed as immutable
--> src/main.rs:4:5
|
3 | let first = &v[0];
| - immutable borrow occurs here
4 | v.push(4);
| ^^^^^^^^^ mutable borrow occurs here
5 | println!("{}", first);
| ----- immutable borrow later used here
読み方:
cannot borrow as mutable: 可変借用できないimmutable borrow occurs here: 行3で不変借用mutable borrow occurs here: 行4で可変借用しようとしたimmutable borrow later used here: 行5で不変借用がまだ使われている
修正: 不変借用を先に使い切る
#![allow(unused)]
fn main() {
let mut v = vec![1, 2, 3];
let first = &v[0];
println!("{}", first); // 先に使う
v.push(4); // その後で変更
}
例3: ライフタイム不足
error[E0106]: missing lifetime specifier
--> src/main.rs:1:33
|
1 | fn longest(x: &str, y: &str) -> &str {
| ---- ---- ^ expected named lifetime parameter
|
= help: this function's return type contains a borrowed value, but the signature does not say whether it is borrowed from `x` or `y`
help: consider introducing a named lifetime parameter
|
1 | fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
| ++++ ++ ++ ++
読み方:
missing lifetime specifier: ライフタイム指定子がないhelp: 戻り値が借用値だが、xとyのどちらか不明consider introducing...: 具体的な修正案
修正: helpの提案通りに
#![allow(unused)]
fn main() {
fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
if x.len() > y.len() { x } else { y }
}
}
例4: トレイト未実装
error[E0277]: `MyStruct` doesn't implement `Debug`
--> src/main.rs:8:22
|
8 | println!("{:?}", my_struct);
| ^^^^^^^^^ `MyStruct` cannot be formatted using `{:?}`
|
= help: the trait `Debug` is not implemented for `MyStruct`
= note: add `#[derive(Debug)]` to `MyStruct` or manually `impl Debug for MyStruct`
読み方:
doesn't implement Debug: Debugトレイトが実装されていないcannot be formatted using {:?}: デバッグフォーマットが使えないnote: deriveするか手動実装するか
修正:
#![allow(unused)]
fn main() {
#[derive(Debug)] // これを追加
struct MyStruct {
value: i32,
}
}
エラーメッセージのパターン
パターン1: 所有権・借用関連
キーワード:
moved,borrow,mutable,immutabledoes not live long enough,lifetime
対処: Phase 2の内容を復習
パターン2: 型関連
キーワード:
mismatched types,expected,foundtrait bound is not satisfied
対処: 型を確認、deriveを追加
パターン3: 未定義・スコープ関連
キーワード:
cannot find,unresolvednot found in this scope
対処: import文の確認、スペルチェック
パターン4: 非同期関連
キーワード:
async,await,FutureSend,Sync
対処: async関数か確認、Send制約を満たす
デバッグ用ツール
型を調べる
#![allow(unused)]
fn main() {
// コンパイルエラーで型を教えてもらう
let x: () = some_expression;
// error: expected `()`, found `ActualType`
}
dbg!マクロ
#![allow(unused)]
fn main() {
let x = dbg!(some_expression);
// [src/main.rs:1] some_expression = value
}
cargo check
# コンパイルせずにエラーチェック(高速)
cargo check
clippy(リンター)
# より詳細な警告と提案
cargo clippy
まとめ
| 読むべき箇所 | 目的 |
|---|---|
| エラーコード | 詳細説明を調べる(rustc --explain) |
| 要約メッセージ | 何が問題かを把握 |
| コードスニペット | どこが問題かを特定 |
| 注釈(-や^) | 原因の詳細を理解 |
| help | 具体的な修正方法 |
心がけ:
- 最初のエラーから順に修正
- helpの提案を積極的に採用
- エラーコードで詳細を調べる
- 落ち着いて読む - Rustコンパイラは親切!
生成AI活用ガイドライン
Rust学習において生成AIを効果的に活用する方法をまとめました。
基本方針
┌─────────────────────────────────────────────────────┐
│ 理解すべき領域 │
│ (AIに頼る前に自分で理解する) │
│ │
│ ・所有権、借用、ライフタイム │
│ ・エラーメッセージの読み方 │
│ ・基本的な型システム │
│ ・プロジェクト構造 │
│ │
├─────────────────────────────────────────────────────┤
│ AI活用推奨領域 │
│ (積極的にAIを活用する) │
│ │
│ ・ボイラープレートコード生成 │
│ ・ライブラリの使い方 │
│ ・テストコード生成 │
│ ・デバッグの補助 │
│ │
└─────────────────────────────────────────────────────┘
人間が必ず理解すべきこと
1. 所有権とメモリ管理
なぜ人間が理解すべきか:
- Rustの核心概念であり、すべてのコードに影響する
- AIの生成コードでも所有権エラーは頻発する
- 理解なしにAIの提案を採用すると、不適切な回避策を使いがち
学習のポイント:
#![allow(unused)]
fn main() {
// この3パターンを完全に理解する
let s1 = String::from("hello");
let s2 = s1; // ムーブ
let s3 = s2.clone(); // クローン
let r = &s3; // 借用
}
2. エラーメッセージの読解
なぜ人間が理解すべきか:
- エラーの原因を理解しないと、同じ間違いを繰り返す
- AIにエラーを丸投げすると、表面的な修正になりがち
- Rustコンパイラのエラーは非常に親切で情報量が多い
実践:
# 必ず自分でエラーを読む習慣をつける
cargo build 2>&1 | less
# エラーコードの詳細を調べる
rustc --explain E0382
3. Result/Optionの扱い
なぜ人間が理解すべきか:
- ほぼすべてのRustコードで使われる
- エラー処理の設計に直接影響
unwrap()の乱用を防ぐ
理解すべきパターン:
#![allow(unused)]
fn main() {
// これらの違いを理解する
result.unwrap() // パニックの可能性
result? // エラー伝播
result.unwrap_or(default) // デフォルト値
match result { ... } // 明示的なハンドリング
}
4. プロジェクト構造
なぜ人間が理解すべきか:
- ファイルの配置がモジュールシステムに直結
- AIは既存の構造を無視した提案をすることがある
- 保守性に大きく影響
理解すべき構造:
src/
├── main.rs # mod宣言
├── lib.rs # ライブラリのルート
├── module.rs # mod module;
└── module/
└── mod.rs # mod module;(ディレクトリ版)
AI活用が効果的な領域
1. ボイラープレートコード
適している理由:
- 定型的なコードは正確に生成できる
- 時間の節約になる
- ミスを減らせる
プロンプト例:
以下の構造体に対して、serde を使った
JSON シリアライゼーションのボイラープレートを生成してください。
struct User {
id: u32,
name: String,
email: String,
}
期待される出力:
#![allow(unused)]
fn main() {
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Serialize, Deserialize)]
struct User {
id: u32,
name: String,
email: String,
}
}
2. ライブラリの使い方
適している理由:
- ドキュメントを読む時間を短縮
- 具体的な使用例を素早く得られる
- 最新のAPIを把握
プロンプト例:
reqwest を使って JSON を POST する方法を教えてください。
エラーハンドリングも含めてください。
3. テストコード生成
適している理由:
- テストケースの網羅性を高められる
- 定型的なテストは正確に生成できる
- テスト駆動開発を加速
プロンプト例:
以下の関数のユニットテストを生成してください。
正常系、異常系、エッジケースを含めてください。
fn divide(a: i32, b: i32) -> Option<i32> {
if b == 0 { None } else { Some(a / b) }
}
4. エラー解決の補助
適している理由:
- エラーメッセージの解釈を補助
- 複数の解決策を提示してもらえる
- 自分で考えた上での確認に使える
プロンプト例:
以下のエラーが出ています。
原因と解決方法を教えてください。
error[E0502]: cannot borrow `v` as mutable because
it is also borrowed as immutable
AIを使う際の注意点
1. 生成コードを必ず理解する
#![allow(unused)]
fn main() {
// AIが生成したコード
async fn fetch_data() -> Result<String, Box<dyn Error>> {
// このコードの各部分を理解できるか?
let response = reqwest::get("https://...")
.await? // <- なぜ?が必要?
.text()
.await?; // <- なぜawaitが2回?
Ok(response)
}
}
チェックリスト:
- なぜこの型が使われているか理解した
- エラー処理の方法を理解した
- 所有権の流れを理解した
2. セキュリティに注意
避けるべきこと:
#![allow(unused)]
fn main() {
// AIが生成しがちな危険なパターン
// パスワードを環境変数からそのまま使用
let password = std::env::var("PASSWORD").unwrap();
// 入力値を検証せずにSQL文を構築
let query = format!("SELECT * FROM users WHERE id = {}", user_input);
// unwrap()の乱用
let data = file.read_to_string().unwrap();
}
正しいアプローチ:
#![allow(unused)]
fn main() {
// 環境変数は適切に処理
let password = std::env::var("PASSWORD")
.expect("PASSWORD environment variable must be set");
// パラメータバインディングを使用
sqlx::query("SELECT * FROM users WHERE id = ?")
.bind(user_input)
// 適切なエラーハンドリング
let data = file.read_to_string()
.map_err(|e| AppError::FileRead(e))?;
}
3. 最新情報かどうか確認
AIの知識には期限があります:
#![allow(unused)]
fn main() {
// 古い書き方かもしれない
extern crate serde; // Rust 2018以降は不要
// 非推奨のAPIかもしれない
std::mem::uninitialized() // 非推奨
// バージョンが古いかもしれない
axum = "0.5" // 最新は0.7
}
確認方法:
- crates.io で最新バージョンを確認
- 公式ドキュメントと照合
cargo clippyで警告をチェック
4. 文脈を伝える
悪いプロンプト:
Rustでログイン機能を作って
良いプロンプト:
Axum 0.7 と SQLx を使った Web API で、
セッションベースのログイン機能を実装したいです。
要件:
- POST /login でメールアドレスとパスワードを受け取る
- パスワードはargon2でハッシュ化されている
- ログイン成功時はセッションにユーザーIDを保存
- 適切なエラーレスポンスを返す
現在のプロジェクト構造:
src/
├── main.rs
├── handlers/
└── models/
使用しているクレート:
- axum = "0.7"
- sqlx = { version = "0.7", features = ["sqlite"] }
- tower-sessions = "0.12"
効果的なプロンプトのテンプレート
コード生成
[言語/フレームワーク]: Rust / Axum 0.7
[目的]: [何を達成したいか]
[制約]: [使用するクレート、バージョン、規約など]
[現在のコード]: [関連するコードがあれば]
期待する出力:
- [出力形式の指定]
- [エラーハンドリングの要件]
- [その他の要件]
デバッグ
[エラーメッセージ]:
[エラーの全文を貼り付け]
[関連するコード]:
```rust
[問題のコード]
### コードレビュー
以下のコードをレビューしてください。
観点:
- 所有権とライフタイムの適切さ
- エラーハンドリング
- パフォーマンス
- Rustらしい書き方か
#![allow(unused)]
fn main() {
[レビュー対象のコード]
}
## 学習段階別のAI活用
### Phase 0-1(入門期)
**推奨度: 低め**
- 基本文法は自分で書く
- エラーは自分で解決を試みる
- AIは「答え合わせ」に使う
### Phase 2(所有権学習期)
**推奨度: 低め**
- 所有権エラーは自分で理解する
- AIの提案より自分の理解を優先
- エラーメッセージの読み方を習得
### Phase 3-4(実践期)
**推奨度: 中程度**
- ボイラープレートはAIに任せる
- ライブラリの使い方はAIに聞く
- 生成コードは必ずレビュー
### Phase 5以降(応用期)
**推奨度: 高め**
- 設計相談にAIを活用
- コードレビューを依頼
- テスト生成を活用
## まとめ
### 人間が理解必須
| 項目 | 理由 |
|------|------|
| 所有権・借用 | Rustの根幹、すべてに影響 |
| エラーメッセージ | 自立したデバッグ能力 |
| Result/Option | エラー処理の基本 |
| プロジェクト構造 | 保守性・拡張性 |
### AI活用推奨
| 項目 | 効果 |
|------|------|
| ボイラープレート | 時間節約 |
| ライブラリ使用例 | 学習効率向上 |
| テスト生成 | 品質向上 |
| デバッグ補助 | 問題解決加速 |
### 鉄則
1. **生成コードは必ず理解してから使う**
2. **セキュリティに関わる部分は特に注意**
3. **文脈を詳しく伝える**
4. **最新情報かどうか確認する**
5. **AIは道具、理解は人間の仕事**