rust

expect()よりunwrap_or_else()を使うべき場合

パフォーマンス上の問題から、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()にわたすクロージャがエラーの中身を受け取るので、||の部分を|_|等に書き換えます。もっとも、Resultexpect()は、Errであった場合にその中身のエラー値のデバッグ情報(Debugトレイトによるもの)を出力してくれるため、それで十分な場合も多いです。

参考

&strではなく遅延評価されるような引数を受け取るようにするexpect()が欲しい、というRFCがあがっていたようです。
Allow any Displayable type for expect.
Closeされた経緯を見る限り、unwrap_or_else()を使うやり方で十分、ということのようです。