コンテンツにスキップ

Rustのpanic

パニック(panic)は、取り返しのつかない問題が起きたときに、Rustがプログラムをすぐに止める仕組みです。これまでのページを試してきた人は、実はもう何度か知らないうちにパニックを起こしているかもしれません。配列の範囲外にアクセスしたり、None に .unwrap() を呼んだりすると、どちらもパニックします。

fn main() {
panic!("深刻な問題が発生しました");
}
thread 'main' (12345) panicked at src/main.rs:2:5:
深刻な問題が発生しました
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace

括弧内の数字(ここでは (12345))はスレッドIDです。実行するたびに変わり、古いバージョンのRustでは表示されません。

▶ 実際に試してみよう

これを実行すると、プログラムはメッセージを表示して止まります。panic! より後のコードは何も実行されません。1行目の thread 'main' (12345) panicked at ... は「メインスレッドが src/main.rs の2行目・5列目でパニックした」という意味で、最後の note: は「環境変数 RUST_BACKTRACE=1 を付けて実行すると、バックトレース(どこから呼ばれたかの履歴)が表示される」という意味です。どちらもRust自身が出すメッセージなので英語のままです。

panic!を直接呼ばなくてもパニックする場面

Section titled “panic!を直接呼ばなくてもパニックする場面”

これまでに出てきたいくつかの操作は、問題があると自動的にパニックします。下の例では、危ない値をコードに直接書くのではなく、関数の引数経由で渡しています。インデックスや 0 をリテラルでその場に書くと、コンパイラはその操作が必ず失敗することをコンパイル時に見抜けることが多く、実行してパニックさせる前に、そもそもビルドを拒否してしまうからです。

fn get(numbers: &[i32], index: usize) -> i32 {
numbers[index] // パニック: インデックスが範囲外
}
fn main() {
let numbers = [1, 2, 3];
println!("{}", get(&numbers, 10));
}
▶ 実際に試してみよう
fn main() {
let value: Option<i32> = None;
println!("{}", value.unwrap()); // パニック: called `Option::unwrap()` on a `None` value
}
▶ 実際に試してみよう
fn divide(a: i32, b: i32) -> i32 {
a / b // パニック: attempt to divide by zero(0で割ろうとした)
}
fn main() {
let a = 10;
let b = 0;
println!("{}", divide(a, b));
}
▶ 実際に試してみよう

どれも、意味をなさない状態のままプログラムを続けさせず、こっそりおかしな値を返すこともしない、というRustの姿勢の表れです。

ここが、これから何度も迫られる本当の判断です。この失敗でプログラムを止めるべきか、それとも呼び出す側に対処するチャンスを与えるべきか。

  • panic!:バグや、コードが正しければ起こるはずのない状況のためのものです。まともに回復する方法がないので、(被害が広がる前に)すぐに止めるのがいちばん安全です。
  • Result(前のページで紹介しました):不正なユーザー入力、ファイルが見つからない、ネットワーク通信の失敗など、呼び出す側が想定していて対処したいと思うのが自然な失敗のためのものです。
// ユーザーからの不正な入力。回復できるのでResultを使う
fn parse_age(input: &str) -> Result<u32, String> {
input.parse().map_err(|_| String::from("正しい年齢ではありません"))
}
// コードが正しければありえない前提条件。panicする
fn get_first(numbers: &[i32]) -> i32 {
if numbers.is_empty() {
panic!("get_firstが空のスライスで呼ばれました");
}
numbers[0]
}

unwrap()とexpect()はパニックへの近道

Section titled “unwrap()とexpect()はパニックへの近道”

Option でも Result でも .unwrap() を見てきました。これは実は「値をちょうだい。なければパニックして」の省略形にすぎません。.expect("メッセージ") も同じことをしますが、もっと役に立つメッセージを添えられます。

fn main() {
let input = "42";
let number: i32 = input.parse().expect("inputは正しい数値のはずです");
println!("{number}");
}
▶ 実際に試してみよう

変換に失敗すると、パニックのメッセージにあなたが書いたテキストが含まれます。.unwrap() が出す一般的なメッセージよりも、デバッグがずっと楽になります。

前提をチェックする:assert!、assert_eq!、unreachable!

Section titled “前提をチェックする:assert!、assert_eq!、unreachable!”

コードのある地点で「ここでは絶対にこうなっているはず」とわかっていることがあります。Rust には、その思い込みをチェックに変えてくれる小さなマクロがいくつかあります。チェックに失敗すると、わかりやすいメッセージ付きでパニックします。そのため、バグがあとで奇妙な結果を引き起こすのではなく、バグが始まったその場所で見つかります。

  • assert!(条件, "メッセージ") は、条件が false ならパニックします。
  • assert_eq!(left, right) は、2つの値が等しくなければパニックし、両方の値を表示します。
  • unreachable!("メッセージ") は、絶対に実行されないはずのコードに印を付けます。ありえない値に対する match のアームなどです。
fn average(numbers: &[i32]) -> i32 {
assert!(!numbers.is_empty(), "average には少なくとも1つの数が必要です");
let mut total = 0;
for n in numbers {
total += n;
}
total / numbers.len() as i32
}
fn day_name(day: u8) -> &'static str {
match day {
1 => "月曜日",
2 => "火曜日",
3 => "水曜日",
4 => "木曜日",
5 => "金曜日",
6 => "土曜日",
7 => "日曜日",
_ => unreachable!("day は1から7のはずですが、{day} でした"),
}
}
fn main() {
let avg = average(&[70, 80, 90]);
assert_eq!(avg, 80);
println!("平均: {avg}");
println!("3日目は{}", day_name(3));
}

実行結果:

平均: 80
3日目は水曜日
▶ 実際に試してみよう

ここではすべてのチェックが通るので、プログラムはふつうに動きます。では、Playground でリストを [70, 80, 95] に変えてみてください。平均が81になり、assert_eq! が2つの値を並べて表示してプログラムを止めます。

thread 'main' (12345) panicked at src/main.rs:26:5:
assertion `left == right` failed
left: 81
right: 80

「left == right というアサーションが失敗した」という意味で、left(左側の値)が81、right(右側の値)が80だったことがわかります。

average(&[]) や day_name(9) も試してみましょう。パニックの中に自分で書いたメッセージが表示されます。

これらのチェックはリリースビルドでも必ず実行されるので、軽い処理にしておきましょう。assert_eq! にはテストのページでまた出会います。テストが「これはあれと等しいはず」と確かめるための、いちばん基本的な方法です。ほかに todo!() もあります。これは “not yet implemented”(まだ実装されていません)というメッセージでパニックします。関数の骨組みだけ先に書いておき、中身はあとで仕上げたいときの仮置きとして便利です。

設定値 let port_text = "8080"; があります。.parse() と .expect(...) を使ってこれを u16 に変換してください。メッセージには、何がまずかったのかがわかる説明を書きます。そのあと ポート8080で待ち受け中 と表示しましょう。ヒント:parse が何を作ればいいのかわかるように、変数に型を付けます(let port: u16 = ...)。うまくいったら、テキストを "80a0" に変えて、パニックに自分のメッセージが出るのを確かめてみましょう。

解答を見る
fn main() {
let port_text = "8080";
let port: u16 = port_text
.parse()
.expect("設定ファイルのポートは0から65535までの数値のはずです");
println!("ポート{port}で待ち受け中");
}

実行結果:

ポート8080で待ち受け中
▶ 実際に試してみよう

ひとりあたりの金額を返す fn split_bill(total: u32, people: u32) -> u32 を書いてください。0人で割り勘するのは呼び出し側のコードのバグなので、わかりやすいメッセージ付きの assert! で捕まえます。合計120を4人で割ったときの金額を表示しましょう。ヒント:割り算の前、関数の1行目に assert! を置きます。

解答を見る
fn split_bill(total: u32, people: u32) -> u32 {
assert!(people > 0, "split_bill には少なくとも1人が必要です");
total / people
}
fn main() {
let share = split_bill(120, 4);
println!("ひとりあたり{share}");
}

実行結果:

ひとりあたり30
▶ 実際に試してみよう
  • panic! は、エラーメッセージを表示してプログラムをすぐに止めます。
  • 標準の操作の中には、自動的にパニックするものがあります(配列の範囲外アクセス、None への .unwrap()、0による割り算など)。
  • ありえないはずのバグや前提条件の破れには panic! を、呼び出す側が対処できる失敗には Result を使いましょう。
  • .expect("メッセージ") は .unwrap() と同じですが、パニックをデバッグしやすくするメッセージを付けられます。
  • assert!、assert_eq!、unreachable! は、正しいと信じていたことが間違っていたときに、わざとパニックを起こします。

理解度チェック

1. panic! を呼ぶとどうなりますか?

2. このページの目安によると、Resultではなくpanic!を使うべきなのはどんなときですか?

3. .unwrap() と .expect("メッセージ") の違いは何ですか?

スコア: 0 / 3