panic マクロの一貫性

概要

  • panic!(..) では常に format_args!(..) が使われるようになりました。つまり、println!() と同じ書き方をすることになります。
  • panic!("{") と書くことはできなくなりました。{{{ とエスケープしなくてはなりません。
  • x が文字列リテラルでないときに、 panic!(x) と書くことはできなくなりました。
    • 文字列でないペイロード付きでパニックしたい場合、 std::panic::panic_any(x) を使うようにしてください。
    • もしくは、xDisplay 実装を用いて、panic!("{}", x) と書いてください。
  • assert!(expr, ..) に関しても同様です。

詳細

panic!() は、Rust で最もよく知られたマクロの一つです。 しかし、このマクロにはいくぶん非直感的な挙動がありましたが、 今までは後方互換性の問題から修正できませんでした。

// Rust 2018
panic!("{}", 1); // Ok, panics with the message "1"
                 // OK。 "1" というメッセージと共にパニックする
panic!("{}"); // Ok, panics with the message "{}"
              // OK。 "{}" というメッセージと共にパニックする

panic!() マクロは、2つ以上の引数が渡されたときだけ、フォーマット文字列を使用します。 引数が1つのときは、引数の中身に見向きもしません。

// Rust 2018
let a = "{";
println!(a); // Error: First argument must be a format string literal
             // エラー: 第一引数は文字列リテラルでなくてはならない
panic!(a); // Ok: The panic macro doesn't care
           // OK: panicマクロは気にしない

その上、このマクロは panic!(123) のように文字列でない引数を渡すこともできました。 このような使い方は稀で、ほとんど役に立ちません。 というのも、この呼び出しで出力されるメッセージはあきれるほど役に立たないからです: panicked at 'Box<Any>' (訳: 'Box<Any> でパニックしました)。

これで特に困るのは、暗黙のフォーマット引数が安定化されたときです。 この機能により、println!("hello {}", name) の代わりに println!("hello {name}") と略記できるようになります。 しかし、panic!("hello {name}") は期待される挙動を示しません。 なぜなら、panic!() は引数が1つだけ与えられたときにそれをフォーマット文字列として扱わないからです。

この紛らわしい状況を解決するために、Rust 2021 の panic!() マクロはより一貫したものになりました。 新しい panic!() マクロは、単一引数として任意の式を受け付けることがなくなりました。 代わりに、println!() と同様に、常に最初の引数をフォーマット文字列として処理するようになりました。 panic!() マクロが任意のペイロードを受け付けるわけではなくなったので、 フォーマット文字列以外のペイロードと共にパニックさせる唯一の方法は、 panic_any()を使うことだけになりました。

// Rust 2021
panic!("{}", 1); // Ok, panics with the message "1"
                 // Ok。"1" というメッセージと共にパニックする
panic!("{}"); // Error, missing argument
              // エラー。引数が足りない
panic!(a); // Error, must be a string literal
           // エラー。文字列リテラルでないといけない

さらに、core::panic!()std::panic!() は Rust 2021 で同一のものになります。 現在は、これらの間には歴史的な違いがあり、 #![no_std] のオンオフを切り替えることで見て取ることができます。

移行

panic への呼び出しのうち、非推奨の挙動を使用していて Rust 2021 ではエラーになる場所に対して、non_fmt_panics というリントが発生します。 Rust 1.50 以降、non_fmt_panics リントはすでにデフォルトで警告として発出されています(後のバージョンではさらにいくつかの機能追加が行われました)。 警告が今現在出ていないコードは、今すぐにでも Rust 2021 に進むことができます!

コードを自動的に Rust 2021 エディションに適合するよう自動移行するか、既に適合するものであることを確認するためには、以下のように実行すればよいです:

cargo fix --edition

手動で移行することを選んだり、そうする必要がある場合には、各 panic の呼び出しについて、 println と同様のフォーマットに書き換えるか、std::panic::panic_any を用いて非文字列型のデータと共にパニックさせるかを選ぶ必要があります。

例えば、panic!(MyStruct) の場合、std::panic::panic_any (これはマクロでなく関数であることに注意)を使うよう書き換えて、std::panic::panic_any(MyStruct) とすればよいです。

パニックのメッセージに波括弧が含まれているのに、引数の個数が一致しない場合は(例: panic!("Some curlies: {}"))、 println! と同じ構文を使うか(つまり panic!("{}", "Some curlies: {}") とするか)、波括弧をエスケープすれば(つまり panic!("Some curlies: {{}}") とすれば)、 その文字列リテラルを用いてパニックすることができます。