パフォーマンス上の問題から、expect()
よりunwrap_or_else()
とpanic!()
の組み合わせを使うべき場合がある、という話。
expect()の失敗時に表示される文字列
Option
型のメソッドであるexpect()
は、値がSome
の場合はその中身を取り出し、そうでない場合は引数として与えられた文字列を表示してパニックを起こします。この文字列の型は&str
であるため、実行時にエラーメッセージを生成したい場合は、format!()
マクロを用いて一旦String
型の値を生成し、その参照を渡してやる必要があります。
例として、次のコードを実行してみます。
fn main() {
foo(0);
}
fn foo(n: i32) {
let a = bar(n).expect(&format!("n = {}", n));
println!("{}", a);
}
fn bar(n: i32) -> Option<i32> {
if n == 0 {
None
} else {
Some(1000 / n)
}
}
この出力は次のようになります。
thread 'main' panicked at 'n = 0', libcore/option.rs:916:5
このように、n = 0だから失敗したのか、とエラーメッセージから判断しやすくなります。
format!()のコスト
上記のコードはこれはこれで正常に動きますが、foo()
が頻繁に呼び出される場合に問題が発生します。というのも、bar()
が正常にSome
を返してくれる場合でも、format!()
が呼び出され、その分余計な処理を行うことになってしまうからです。format!()
はヒープを確保して文字列を整形してくれる分、その実行コストはなかなか大きいです。つまり、頻繁に実行される部分では、expect+formatの組み合わせを用いるのはよろしくないということです。
というわけで、実際にNone
が返されたとわかった場合のみエラーメッセージを生成するようにしなければなりません。この簡単な解決策は、if let文を用いることです。if let文を用いれば、上記のfoo()
は以下のように書けます。
fn foo(n: i32) {
let a = if let Some(a) = bar(n) {
a
} else {
panic!("n = {}", n);
};
println!("{}", a);
}
panic!()
マクロが実行されると即座にパニックを起こします。またこのマクロはprintln!()
やformat!()
と同じように文字列を整形し、エラーとして表示してくれます。
unwrap_or_elseと組み合わせる
if let文を用いる方法でパフォーマンス上の問題は無くなりましたが、いささか冗長になってしまいます。できれば1行で簡潔に書く方法が欲しいところです。そこで、Option
型のメソッドの1つであるunwrap_or_else()
を用います。unwrap_or_else()
はクロージャを引数として受け取り、Option
の中身がNone
であった場合、代わりの値をクロージャで生成して返してくれます。また、Option
の中身がSome
であった場合、クロージャは実行されません。この性質を用いると、foo()
は以下のように書けます。
fn foo(n: i32) {
let a = bar(n).unwrap_or_else(|| panic!("n = {}", n));
println!("{}", a);
}
クロージャであることを表す||
という記号が残ってしまいますが、これで1行におさめることができました。
なお、本来unwrap_or_else
に渡すクロージャの戻り値は、Option<T>
のT
と同じでなければなりませんが、panic!()
は "!
" という特殊な型として扱われるため、問題なくコンパイルが通ります。
Resultの場合
Result
にもunwrap_or_else()
があるため、同じくpanic!()
と合わせた書き方が可能です。その場合、unwrap_or_else()
にわたすクロージャがエラーの中身を受け取るので、||
の部分を|_|
等に書き換えます。もっとも、Result
のexpect()
は、Err
であった場合にその中身のエラー値のデバッグ情報(Debug
トレイトによるもの)を出力してくれるため、それで十分な場合も多いです。
参考
&str
ではなく遅延評価されるような引数を受け取るようにするexpect()
が欲しい、というRFCがあがっていたようです。
Allow any Displayable type for expect.
Closeされた経緯を見る限り、unwrap_or_else()
を使うやり方で十分、ということのようです。