コンテンツにスキップ

RustのResult

Option が「ないかもしれない値」を表すのに対して、Result は「失敗するかもしれない処理」を表します。そして、値がないだけの場合とは違い、失敗にはたいてい理由がついてきます。

Result も標準ライブラリにある列挙型(enum)です。今回は、2つのバリアントがどちらもデータを持っています。

enum Result<T, E> {
Ok(T), // 成功。値を持つ
Err(E), // 失敗。エラーを持つ
}

型変換のページで出てきた .parse() は、ちょうどいい実例です。テキストを数値に変換する処理は、そのテキストが数値でなければ失敗します。

fn main() {
let good: Result<i32, std::num::ParseIntError> = "42".parse();
let bad: Result<i32, std::num::ParseIntError> = "abc".parse();
println!("{:?}", good);
println!("{:?}", bad);
}

実行結果:

Ok(42)
Err(ParseIntError { kind: InvalidDigit })
▶ 実際に試してみよう

InvalidDigit は「数字として正しくない文字が含まれている」という意味で、Rust自身が出すメッセージなので英語のままです。

fn main() {
let input = "42";
match input.parse::<i32>() {
Ok(number) => println!("変換できました: {number}"),
Err(e) => println!("変換に失敗しました: {e}"),
}
}
▶ 実際に試してみよう

Option を match で扱うときと同じ形です。ただし Err のアームでは、「何もなかった」というだけでなく、中身を調べたり報告したりできる実際のエラー値を受け取れます。

fn divide(a: f64, b: f64) -> Result<f64, String> {
if b == 0.0 {
Err(String::from("0で割ることはできません"))
} else {
Ok(a / b)
}
}
fn main() {
match divide(10.0, 2.0) {
Ok(result) => println!("結果: {result}"),
Err(e) => println!("エラー: {e}"),
}
match divide(10.0, 0.0) {
Ok(result) => println!("結果: {result}"),
Err(e) => println!("エラー: {e}"),
}
}

実行結果:

結果: 5
エラー: 0で割ることはできません
▶ 実際に試してみよう

Result<f64, String> を返すことで、呼び出す側は必ず失敗の可能性に向き合うことになります。0による割り算を、うっかり普通の数値として扱ってしまうことはありえません。

失敗するかもしれない処理をいくつもつなげると、match がどんどん深く入れ子になってしまいます。? 演算子を使えば、それを平らにできます。? の意味は「これが Err なら、そのエラーを今の関数からすぐに返す。そうでなければ Ok の中の値をちょうだい」です。

fn parse_and_double(input: &str) -> Result<i32, std::num::ParseIntError> {
let number = input.parse::<i32>()?; // 変換に失敗したら、ここで早めにreturnする
Ok(number * 2)
}
fn main() {
println!("{:?}", parse_and_double("21"));
println!("{:?}", parse_and_double("oops"));
}

実行結果:

Ok(42)
Err(ParseIntError { kind: InvalidDigit })
▶ 実際に試してみよう

? がなければ、number を取り出すか、エラーと一緒に早めに抜けるかのためだけに、この行にも match が必要になります。? はそれをたった1文字でやってくれるのです。ただし、? が使えるのは、その関数自体が Result(または Option)を返す場合だけです。そうでないと、エラーを返す先がないからです。

Rustには panic! もあります。これはプログラムをすぐに止めるもので、次のページで詳しく説明します。目安としては、呼び出す側が対処したいと思うのが自然な失敗(不正な入力、ファイルが見つからないなど)には Result を使いましょう。panic! は、本当にバグがあることを示していて、処理を続けるよりも止めたほうがましな状況のためにとっておきます。

関数が何通りもの理由で失敗するようになると、ただの String のエラーでは足りなくなってきます。自分でエラー型を定義する方法は独自のエラー型のページで、多くの Rust プロジェクトが決まりきったコードを減らすために使っているクレートはanyhowとthiserrorのページで紹介しています。

fn parse_age(input: &str) -> Result<u32, String> を書いてください。テキストが数値でないとき、または数値が150を超えるときは、わかりやすいメッセージ付きの Err を返します。"34"、"abc"、"200" で試してみましょう。ヒント:input.parse::<u32>() を match して、Ok のアームの中で if を使って大きさをチェックします。

解答を見る
fn parse_age(input: &str) -> Result<u32, String> {
match input.parse::<u32>() {
Ok(age) => {
if age > 150 {
Err(format!("{age}歳というのは現実的ではありません"))
} else {
Ok(age)
}
}
Err(_) => Err(format!("'{input}' は数値ではありません")),
}
}
fn main() {
for input in ["34", "abc", "200"] {
match parse_age(input) {
Ok(age) => println!("年齢: {age}"),
Err(e) => println!("エラー: {e}"),
}
}
}

実行結果:

年齢: 34
エラー: 'abc' は数値ではありません
エラー: 200歳というのは現実的ではありません
▶ 実際に試してみよう

2つの文字列を数値に変換して、その合計を返す fn add_strings(a: &str, b: &str) -> Result<i32, std::num::ParseIntError> を書いてください。match ではなく ? を使います。("10", "20") と ("10", "twenty") の結果を表示しましょう。ヒント:parse 1回につき ? を1つ付けます。最後の行で合計を Ok(...) で包みます。

解答を見る
fn add_strings(a: &str, b: &str) -> Result<i32, std::num::ParseIntError> {
let x = a.parse::<i32>()?;
let y = b.parse::<i32>()?;
Ok(x + y)
}
fn main() {
println!("{:?}", add_strings("10", "20"));
println!("{:?}", add_strings("10", "twenty"));
}

実行結果:

Ok(30)
Err(ParseIntError { kind: InvalidDigit })
▶ 実際に試してみよう

Err(ParseIntError { kind: InvalidDigit }) は、標準ライブラリのエラーを {:?} で表示したものです。「数字として使えない文字が含まれていた」という意味です。

  • Result<T, E> は、成功(Ok)するか、エラー値とともに失敗(Err)するかもしれない処理を表します。
  • match で両方のケースをはっきり扱います。Option と同じパターンです。
  • ? 演算子は Err を自動的に早めに返すので、失敗しうる呼び出しのたびに match を書く必要がありません。
  • 呼び出す側が回復できそうなエラーには、panic! よりも Result を使いましょう。

理解度チェック

1. Result<T, E> の2つのバリアントはどれですか?

2. Resultを返す関数の中で、? 演算子は何をしますか?

3. このページによると、panic! よりも Result を使うべきなのはどんなときですか?

4. このページの divide の例で、divide(10.0, 0.0) は何を返しますか?

5. ? 演算子が、Result(またはOption)を返す関数の中でしか使えないのはなぜですか?

スコア: 0 / 5