Rust 基礎講座 #6 エラー処理 — Result と ? 演算子、panic と unwrap の境界
Rust には try/catch がありません。例外という概念自体がないからです。代わりに前回の Option と同じ原理で、失敗しうるという事実を戻り値の型に現し、コンパイラが処理を強制します。 最初は面倒に見えますが、「この関数がどんな状況で失敗するのか」がシグネチャを見るだけですべて分かるコードになります。
Result — 成功または失敗 #
enum Result<T, E> {
Ok(T), // 成功。結果の値を持つ
Err(E), // 失敗。エラーの値を持つ
}ファイルの読み取り、ネットワークリクエスト、パースのように失敗しうる関数は、すべて Result を返します。第 2 回から使ってきた parse がそうです。
let input = "42";
let n: i32 = match input.parse() {
Ok(value) => value,
Err(e) => {
println!("数値ではありません: {e}");
return;
}
};例外方式との違いがここに現れます。例外は処理を忘れてもコンパイルが通り、ランタイムのどこかで爆発します。Result は i32 ではなく Result<i32, ParseIntError> という別の型なので、失敗の可能性を剥がすまでは値を使えません。呼び出した側が処理を忘れることが、コンパイル段階で不可能なのです。
? 演算子 — エラーは上へ渡す #
すべての呼び出しに match を書くと、コードはすぐ階段になります。実務のエラー処理の大半は「ここでは処理せず、呼び出し元に渡す」ですが、その伝播を 1 文字に縮めたのが ? 演算子です。
use std::fs;
fn read_config() -> Result<Config, ConfigError> {
let text = fs::read_to_string("config.toml")?; // Err ならここで即座に返す
let config = parse_config(&text)?; // Err ならここで即座に返す
Ok(config)
}? は、結果が Ok なら中の値を取り出して先へ進み、Err ならそのエラーを関数の戻り値として即座に返します。 上の関数を match で展開すると十数行になりますが、? 2 つで同じ意味になります。条件が 1 つあります。関数の戻り値の型が Result であることです。エラーを引き渡す先がシグネチャに宣言されていなければならないからです。普通の関数で ? を使うと「この関数は Result を返さない」というコンパイルエラー(E0277)になり、help 行が戻り値の型の変更を提案してくれます。
伝播は main まで続けられます。main も Result を返せるので、小さなプログラムはこの形が実用的な基本形です。
use std::error::Error;
fn main() -> Result<(), Box<dyn Error>> {
let config = read_config()?;
run(config)?;
Ok(())
}Box<dyn Error> は「どんな種類でもエラーならよい」という実用的な型です(dyn の正体は第 8 回のトレイトで扱います)。異なるエラー型が混ざるプログラムのエントリーポイントでよく使う形で、エラーが main まで上がってくると、プロセスはメッセージを出力して失敗コードで終了します。
unwrap と expect — 近道の使用条件 #
let n: i32 = "42".parse().unwrap(); // Err ならパニック
let n: i32 = "42".parse().expect("ポート番号のパースに失敗"); // メッセージを残す unwrap
unwrap は失敗処理を放棄して「失敗したらそのまま死ね」を選ぶことです。無責任に見えますが、正当な使いどころがあります。
- 例、プロトタイプ、テスト: エラー処理がコードの本題ではないとき。この講座のこれまでの回がそうでした。
- 失敗がそのままプログラムのバグであるとき: 直前に入れたキーをすぐ取り出す場合のように、失敗したならロジック自体が壊れている状況。このときは
expect("直前に入れたキーがない")のように理由を残す方が良いです。
逆に、ユーザー入力、ファイル、ネットワークのように正常運用中でも失敗しうる箇所の unwrap は時限爆弾です。いつか形式の崩れた入力 1 つでプロセス全体が落ちます。こうした箇所は ? で伝播するか、match で回復するのが定石です。
panic — 回復ではなく中断 #
panic! はプログラムを即座に中断させる装置で、unwrap の失敗がまさにこれです。配列の範囲外インデックスアクセス(第 2 回)もパニックでした。Rust の区分基準は明確です。
- 予期できる失敗(存在しないファイル、不正な入力、切れた接続)→
Resultで表現し、処理を強制します。 - 起きてはならない状態(不変条件の違反、論理的にありえない分岐)→
panic!で即座に中断します。壊れた状態で動き続けるより、死ぬ方が安全だからです。
ライブラリを作るなら、この区分はさらに重要になります。呼び出し元が回復できるはずの失敗をパニックで処理すると、呼び出し元から選択権を奪う設計になってしまいます。
まとめ #
- Rust に例外はありません。失敗の可能性は
Result<T, E>という戻り値の型に現れ、処理しなければ値そのものを使えません。 ?演算子は、Okなら値を取り出し、Errなら即座に返す伝播の装置です。戻り値の型がResultの関数の中でだけ使えます。- 小さなプログラムは
main() -> Result<(), Box<dyn Error>>が実用的な基本形です。 unwrap・expectが正当なのは、例・テストであるか、失敗がそのままバグである箇所だけです。入力・ファイル・ネットワークには?とmatchが定石です。- 予期できる失敗は
Result、起きてはならない状態はpanicという境界が、Rust のエラー設計の軸です。 - 次回は実務コードの本体であるコレクションとイテレータを扱います。
Vec、String、HashMap、そして for 文を置き換えるチェーンスタイルまで見ていきます。