Skip to content

Commit 9ac920f

Browse files
committed
rust-12まで
1 parent 54ec68b commit 9ac920f

File tree

6 files changed

+1246
-3
lines changed

6 files changed

+1246
-3
lines changed

app/pagesList.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -104,6 +104,10 @@ export const pagesList = [
104104
{ id: 6, title: "構造体とメソッド構文" },
105105
{ id: 7, title: "列挙型とパターンマッチ" },
106106
{ id: 8, title: "モジュールシステムとパッケージ管理" },
107+
{ id: 9, title: "コレクションと文字列" },
108+
{ id: 10, title: "エラーハンドリング" },
109+
{ id: 11, title: "ジェネリクスとトレイト" },
110+
{ id: 12, title: "ライフタイム" },
107111
],
108112
},
109113
] as const;

public/docs/rust-10.md

Lines changed: 337 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,337 @@
1+
# 第10章:エラーハンドリング
2+
3+
ようこそ、第10章へ。ここまでRustの所有権や型システムについて学んできましたが、この章では実用的なアプリケーション開発において避けては通れない「エラーハンドリング」について解説します。
4+
5+
他の多くの言語(Java, Python, C++など)とRustが最も大きく異なる点の一つが、**「例外(Exception)」が存在しない**ことです。
6+
7+
Rustでは、エラーは「誰かがキャッチしてくれることを祈って投げるもの」ではなく、**「戻り値として明示的に処理すべき値」**として扱われます。この設計思想により、予期せぬクラッシュを防ぎ、堅牢なソフトウェアを構築することができます。
8+
9+
## エラーの分類
10+
11+
Rustでは、エラーを大きく2つのカテゴリーに分類します。
12+
13+
1. **回復不可能なエラー (Unrecoverable Errors):**
14+
* バグ、配列の範囲外アクセス、メモリ不足など。
15+
* プログラムは即座に停止すべき状況。
16+
* 手段: `panic!`
17+
2. **回復可能なエラー (Recoverable Errors):**
18+
* ファイルが見つからない、パースの失敗、ネットワーク切断など。
19+
* 呼び出し元で対処(リトライやエラーメッセージ表示)が可能な状況。
20+
* 手段: `Result<T, E>`
21+
22+
## 回復不可能なエラー (`panic!`)
23+
24+
プログラムが続行不可能な状態に陥った場合、Rustはパニック(panic)を起こします。これはデフォルトでスタックを巻き戻し(unwind)、データを掃除してからプログラムを終了させます。
25+
26+
もっとも単純な方法は `panic!` マクロを呼ぶことです。
27+
28+
```rust:panic_demo.rs
29+
fn main() {
30+
println!("処理を開始します...");
31+
32+
// 何か致命的なことが起きたと仮定
33+
panic!("ここで致命的なエラーが発生しました!");
34+
35+
// 以下の行は実行されません
36+
println!("この行は表示されません");
37+
}
38+
```
39+
40+
```rust-exec:panic_demo.rs
41+
処理を開始します...
42+
thread 'main' panicked at panic_demo.rs:5:5:
43+
ここで致命的なエラーが発生しました!
44+
```
45+
46+
**ポイント:**
47+
48+
* `panic!` は、基本的に「プログラムのバグ」や「どうしようもない状況」でのみ使用します。
49+
* 通常の制御フロー(入力値のバリデーション失敗など)には使用しません。
50+
51+
## 回復可能なエラー (`Result<T, E>`)
52+
53+
Rustのエラーハンドリングの主役は `Result` 列挙型です。以前の章で学んだ `Option` に似ていますが、失敗した場合に「なぜ失敗したか(エラー内容)」を持つ点が異なります。
54+
55+
定義は以下のようになっています(標準ライブラリに含まれています)。
56+
57+
```rust
58+
enum Result<T, E> {
59+
Ok(T), // 成功時:値 T を含む
60+
Err(E), // 失敗時:エラー E を含む
61+
}
62+
```
63+
64+
### 基本的な `Result` の処理
65+
66+
他の言語での `try-catch` の代わりに、Rustでは `match` 式を使って成功と失敗を分岐させるのが基本です。
67+
68+
```rust:result_basic.rs
69+
fn divide(numerator: f64, denominator: f64) -> Result<f64, String> {
70+
if denominator == 0.0 {
71+
// 失敗時は Err でラップして返す
72+
return Err(String::from("0で割ることはできません"));
73+
}
74+
// 成功時は Ok でラップして返す
75+
Ok(numerator / denominator)
76+
}
77+
78+
fn main() {
79+
let inputs = vec![(10.0, 2.0), (5.0, 0.0)];
80+
81+
for (num, den) in inputs {
82+
let result = divide(num, den);
83+
84+
match result {
85+
Ok(val) => println!("{} / {} = {}", num, den, val),
86+
Err(e) => println!("エラー: {}", e),
87+
}
88+
}
89+
}
90+
```
91+
92+
```rust-exec:result_basic.rs
93+
10 / 2 = 5
94+
エラー: 0で割ることはできません
95+
```
96+
97+
この明示的な分岐により、プログラマはエラー処理を「忘れる」ことができなくなります(コンパイラが `Result` を無視すると警告を出したり、使おうとすると型エラーになるため)。
98+
99+
## `unwrap``expect` の使い所
100+
101+
毎回 `match` で分岐を書くのは冗長な場合があります。「失敗したらプログラムをクラッシュさせていい」という場合や、「ここでは絶対に失敗しない」と確信がある場合のために、ヘルパーメソッドが用意されています。
102+
103+
### `unwrap`
104+
105+
`Result``Ok` なら中身を返し、`Err` なら即座に `panic!` します。手っ取り早いですが、エラーメッセージは一般的で詳細が含まれません。
106+
107+
### `expect`
108+
109+
`unwrap` と同じ挙動ですが、パニック時に表示するメッセージを指定できます。**デバッグのしやすさから、通常は `unwrap` よりも `expect` が推奨されます。**
110+
111+
```rust:unwrap_expect.rs
112+
fn main() {
113+
let valid_str = "100";
114+
let invalid_str = "hello";
115+
116+
// 1. unwrap: 成功時は値を返す
117+
let n: i32 = valid_str.parse().unwrap();
118+
println!("パース成功: {}", n);
119+
120+
// 2. expect: 失敗時は指定したメッセージと共に panic! する
121+
// 以下の行を実行するとクラッシュします
122+
let _m: i32 = invalid_str.parse().expect("数値のパースに失敗しました");
123+
}
124+
```
125+
126+
```rust-exec:unwrap_expect.rs
127+
thread 'main' panicked at unwrap_expect.rs:11:35:
128+
数値のパースに失敗しました: ParseIntError { kind: InvalidDigit }
129+
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
130+
```
131+
132+
**使い所のヒント:**
133+
134+
* **プロトタイピング・実験:** `unwrap` を多用してロジックを素早く組む。
135+
* **テストコード:** テスト中に失敗したらテスト自体を落としたいので `unwrap` が有用。
136+
* **「絶対に失敗しない」ロジック:** 理論上エラーにならない場合でも、型合わせのために `unwrap` が必要な場合があります。
137+
138+
## エラーの伝播(`?` 演算子)
139+
140+
関数内でエラーが発生した際、その場で処理せずに呼び出し元へエラーを返したいことがよくあります。これを「エラーの伝播(propagation)」と呼びます。
141+
142+
Rustにはこれを劇的に短く書くための **`?` 演算子** があります。
143+
144+
* `Result` 値の後ろに `?` を置く。
145+
* 値が `Ok` なら、中身を取り出して処理を続行。
146+
* 値が `Err` なら、**その関数から即座に `return Err(...)` する。**
147+
148+
<!-- end list -->
149+
150+
```rust:error_propagation.rs
151+
use std::num::ParseIntError;
152+
153+
// 文字列を受け取り、最初の文字を切り出して数値に変換し、10倍して返す
154+
fn get_first_digit_scaled(text: &str) -> Result<i32, String> {
155+
// 1. 文字列が空の場合のエラー処理
156+
let first_char = text.chars().next().ok_or("空の文字列です".to_string())?;
157+
158+
// 2. 文字を数値にパース(失敗したらエラー伝播)
159+
// ParseIntError を String に変換するために map_err を使用しています
160+
// (?演算子はFromトレイトを使って自動変換を行いますが、ここでは単純化のため手動変換します)
161+
let num: i32 = first_char.to_string()
162+
.parse()
163+
.map_err(|_| format!("'{}' は数値ではありません", first_char))?;
164+
165+
Ok(num * 10)
166+
}
167+
168+
fn main() {
169+
match get_first_digit_scaled("5 apples") {
170+
Ok(v) => println!("計算結果: {}", v),
171+
Err(e) => println!("エラー発生: {}", e),
172+
}
173+
174+
match get_first_digit_scaled("banana") {
175+
Ok(v) => println!("計算結果: {}", v),
176+
Err(e) => println!("エラー発生: {}", e),
177+
}
178+
}
179+
```
180+
181+
```rust-exec:error_propagation.rs
182+
計算結果: 50
183+
エラー発生: 'b' は数値ではありません
184+
```
185+
186+
`?` 演算子を使うことで、`match` のネスト地獄(右方向へのドリフト)を防ぎ、コードの流れを「成功ルート」を中心に記述できます。
187+
188+
## カスタムエラー型の定義
189+
190+
ライブラリや大規模なアプリケーションを作る場合、`String` 型のエラーでは情報が不足します。Rustでは `std::error::Error` トレイトを実装した独自の型(通常は Enum)を定義するのが一般的です。
191+
192+
Rustのエコシステムでは、ボイラープレート(定型コード)を減らすために **`thiserror`** というクレートが非常に人気ですが、ここでは仕組みを理解するために標準機能だけで実装してみます。
193+
194+
```rust:custom_error.rs
195+
use std::fmt;
196+
197+
// 1. 独自のエラー型を定義
198+
#[derive(Debug)]
199+
enum MyToolError {
200+
IoError(String),
201+
ParseError(String),
202+
LogicError,
203+
}
204+
205+
// 2. Display トレイトの実装(ユーザー向けのエラーメッセージ)
206+
impl fmt::Display for MyToolError {
207+
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
208+
match self {
209+
MyToolError::IoError(msg) => write!(f, "IOエラー: {}", msg),
210+
MyToolError::ParseError(msg) => write!(f, "解析エラー: {}", msg),
211+
MyToolError::LogicError => write!(f, "ロジックエラーが発生しました"),
212+
}
213+
}
214+
}
215+
216+
// 3. Error トレイトの実装(これを実装することで、Rustのエラーエコシステムと統合される)
217+
impl std::error::Error for MyToolError {}
218+
219+
// 使用例
220+
fn dangerous_operation(input: i32) -> Result<String, MyToolError> {
221+
match input {
222+
0 => Err(MyToolError::IoError("ディスク書き込み失敗".into())),
223+
1 => Err(MyToolError::ParseError("不正なヘッダ".into())),
224+
2 => Err(MyToolError::LogicError),
225+
_ => Ok("成功!".into()),
226+
}
227+
}
228+
229+
fn main() {
230+
let results = [dangerous_operation(0), dangerous_operation(3)];
231+
232+
for res in results {
233+
match res {
234+
Ok(s) => println!("結果: {}", s),
235+
Err(e) => println!("失敗: {}", e), // Displayの実装が使われる
236+
}
237+
}
238+
}
239+
```
240+
241+
```rust-exec:custom_error.rs
242+
失敗: IOエラー: ディスク書き込み失敗
243+
結果: 成功!
244+
```
245+
246+
**補足:** `thiserror` クレートを使うと、上記の `impl fmt::Display` などをマクロで自動生成でき、以下のように簡潔に書けます(参考情報)。
247+
248+
```rust
249+
// thiserrorを使った場合のイメージ
250+
#[derive(thiserror::Error, Debug)]
251+
enum MyToolError {
252+
#[error("IOエラー: {0}")]
253+
IoError(String),
254+
// ...
255+
}
256+
```
257+
258+
## この章のまとめ
259+
260+
* **例外はない**: Rustは戻り値 `Result<T, E>` でエラーを表現する。
261+
* **Panic**: 回復不可能なエラーには `panic!` を使うが、乱用しない。
262+
* **Result処理**: 基本は `match` で処理する。
263+
* **便利メソッド**: `unwrap` は強制取り出し(失敗時パニック)、`expect` はメッセージ付きパニック。
264+
* **?演算子**: エラーが発生したら即座に呼び出し元へ `Err` を返すショートカット。
265+
* **型安全性**: コンパイラがエラー処理の漏れを指摘してくれるため、堅牢なコードになる。
266+
267+
### 練習問題 1: 安全な割り算
268+
269+
2つの `f64` を受け取り、割り算の結果を返す関数 `safe_div` を作成してください。
270+
271+
* 戻り値は `Result<f64, String>` としてください。
272+
* 0で割ろうとした場合は、「Division by zero」というエラーメッセージを含む `Err` を返してください。
273+
* `main` 関数で、正常なケースとエラーになるケースの両方を呼び出し、結果を表示してください。
274+
275+
<!-- end list -->
276+
277+
```rust:practice10_1.rs
278+
fn safe_div(a: f64, b: f64) -> Result<f64, String> {
279+
// ここにコードを書いてください
280+
281+
}
282+
283+
fn main() {
284+
let test_cases = vec![(10.0, 2.0), (5.0, 0.0)];
285+
286+
for (a, b) in test_cases {
287+
match safe_div(a, b) {
288+
Ok(result) => println!("{} / {} = {}", a, b, result),
289+
Err(e) => println!("エラー: {}", e),
290+
}
291+
}
292+
}
293+
```
294+
295+
```rust-exec:practice10_1.rs
296+
10 / 2 = 5
297+
エラー: Division by zero
298+
```
299+
300+
### 練習問題 2: データ処理チェーン(エラー伝播)
301+
302+
文字列形式の数値(例: "10,20,30")を受け取り、カンマ区切りの最初の数値を2倍にして返す関数 `process_csv_data` を作成してください。
303+
304+
* 以下の手順を行い、途中でエラーがあれば `?` 演算子などを使って伝播させてください。
305+
1. 文字列をカンマ `,` で分割し( `split` メソッド)、最初の要素を取得する(要素がない場合はエラー)。
306+
2. 取得した文字列を `i32` にパースする( `parse` メソッド)(パース失敗はエラー)。
307+
3. 数値を2倍して返す。
308+
* 関数の戻り値は `Result<i32, String>` とします(エラー型の変換が必要な場合は `map_err` を活用してください)。
309+
310+
<!-- end list -->
311+
312+
```rust:practice10_2.rs
313+
fn process_csv_data(csv: &str) -> Result<i32, String> {
314+
315+
316+
}
317+
318+
fn main() {
319+
let inputs = ["10,20,30", "abc,20", "", " ,50"];
320+
321+
for input in inputs {
322+
print!("Input: {:<10} => ", format!("\"{}\"", input));
323+
match process_csv_data(input) {
324+
Ok(n) => println!("結果: {}", n),
325+
Err(e) => println!("エラー: {}", e),
326+
}
327+
}
328+
}
329+
```
330+
331+
```rust-exec:practice10_2.rs
332+
Input: "10,20,30" => 結果: 20
333+
Input: "abc,20" => エラー: 'abc' は数値ではありません
334+
Input: "" => エラー: 要素が空です
335+
Input: " ,50" => エラー: 要素が空です
336+
```
337+

0 commit comments

Comments
 (0)