LoginSignup
26
16

あなたのRustすぐにパニックしませんか? - unwrap of Death -

Posted at

はじめに

Rustはクラウド業界から組込み業界まで幅広く使われるようになりました。

こんなにコンパイラに怒られまくるマゾい言語が流行っているなんて、最高です。お前ら。

私はお仕事でRustを使っており、Rust布教をしています。

Rustは安全と言われていますが、すぐにPanicするプログラムに溢れています。サンプルコードをコピペすると簡単にPanicするプログラムが出来上がります。なぜでしょうか?

この記事ではunwrap警察として、世の中のRustからPanicを減らす方法について書いてみます。

アンラッパー

Optional型がある言語(SwiftとかJava8とか)から来た人を除いて、Rustのtry/catchではなく、OptionやResultでエラー処理をするスタイルに違和感を覚えた人も多いでしょう。

Option::unwrap()

まずはRust基本テクニックのOption::unwrap()です。これはOptionSomeなら値を返して、ErrだったらPanicします。

「このunwrap()はオマジナイ」 と聞いたら、unwrap警察の出番です。

    let something:Option<i32> = Some(3);
    println!("something:{}", something.unwrap());

    let something:Option<i32> = None;
    println!("something:{}", something.unwrap()); // <- Panic!

この例ではシンプルかもしれませんが、

something.unwrap().unwrap().unwrap().do_something()

こういうchainになったとき、より難解さが向上します。

Result::unwrap()

次に戦わないといけないのはResult::unwrap()です。
Rustのエラーハンドリングは、まだ過渡期で、振り回されている人も多いでしょう。

Resultは以下のように正常値の型と、エラーの型を指定します。


let my_result:Result<i32, String> = Ok(123); // or Err("my error")

これもunwrap()の温床になります。

let something:Result<i32, String> = Ok(3);
println!("something:{}", something.unwrap());

let something:Result<i32, String> = Err("error desu".to_string());
println!("something:{}", something.unwrap()); // <- Panic

こんな感じでPanicします。

Rustではエラー型の定義や補足が難しいですね。

アンラッパーを捕まえる

気軽にunwrap()することで、流れるようにpanicするコードになってしまいます。

プログラマの犯罪に、tryするけどcatchで無視がありますが、unwrap()も同レベルの犯罪とみなされる日も近いです。

あなたは今から、気をつけてunwrap()を使うでしょう。
大きな組織での開発になったとき、どのようにするべきでしょうか?

unwrap()を禁止しよう

やや過激にunwrapを禁止してみましょう。

Clippyでは以下のコマンドで、unwrap()をエラーにすることができます。

$ cargo clippy -- -D clippy::unwrap_used

error: used `unwrap()` on a `Result` value
 --> src/main.rs:8:30
  |
8 |     println!("something:{}", something.unwrap());
  |                              ^^^^^^^^^^^^^^^^^^

世の中には、unwrap()を禁止するためのcargoモジュールを作っている方もおり、unwrap()が人類の敵だとおわかりでしょうか。

unwrap()がない世界での生き方

とは言え、unwrap()も目的があって使われていたので、代わりの実装が必要になります。

unwrap() of Death

まず簡単なケースは、本当にPanicで死んでいいケースです。

次の例は環境変数MY_ENVが実行に必須であり、ない場合は終了していいケースです。

let env = std::env::var("MY_ENV").expect("You must set MY_ENV. Abort.");

ですが、これだとスタックトレースも出てしまいます。ユーザー層によってはスタックトレースを表示すると(人間が)パニックを起こします。

Rust1.65.0で導入された let-else構文を使って以下のように書いてみましょう。

let Ok(env) = std::env::var("MY_ENV") else {
    eprintln!("You must set MY_ENV. Abort.");
    std::process::exit(1);
};

// 以降、envはStringとして使える

これだと丁寧に終了できます。終了コードも設定できます。

unwrap()だけでなく、expect()panic!()も取り締まりたい場合は、以下のようにClippyを使います。

$ cargo clippy -- -D clippy::expect_used -D clippy::panic  -D clippy::unwrap_used

unwrap()と戦わないといけないケース

OptionResultを正しく皮むきし、エラー処理が必要なケースです。

Rustではmatch構文やif-let構文、先述したlet-else構文が使えます。

多くの場合はこれらの構文でカバーしましょう。

//matchケース
let something:Result<i32, String> = Ok(3);
match something {
    Ok(i) => println!("something:{}", i),
    Err(e) => eprintln!("error:{}", e),
}

//if-letケース
let something:Result<i32, String> = Ok(3);
if let Ok(i) = something {
    println!("something:{}", i);
}
//エラー処理してないよ

ただ、OptionResultが入れ子になっている場合は大変です。

この例のように、入れ子で変数を定義することはなくても、処理の途中で入れ子になるケースがあります。

let something:Option<Result<Result<i32, String>, String>> = Some(Ok(Ok(3)));

match something{
    Some(o2)=>{
        match o2{
            Ok(o3)=>{
                match o3{
                    Ok(i)=>{
                        println!("something:{}", i);
                    }
                    Err(e)=>{
                        eprintln!("error in 2nd Result:{}", e);
                    }
                }
            }
            Err(e)=>{
                eprintln!("error in 1st Result:{}", e);
            }
        }
    }
    None=>{
        eprintln!("1st Option is None");
    }
}

この場合、matchは入れ子でパターン指定できるので、以下のように短く書けるケースもあります。

let something:Option<Result<Result<i32, String>, String>> = Some(Ok(Ok(3)));

match something{
    Some(Ok(Ok(i)))=>{
        println!("something:{}", i);
    }
    Some(Ok(Err(e)))=>{
        eprintln!("error in 2nd Result:{}", e);
    }
    Some(Err(e))=>{
        eprintln!("error in 1st Result:{}", e);
    }
    None=>{
        eprintln!("1st Option is None");
    }
}

復習:アンラッパーのコード

ここでunwrap()のコードを復習してみます。

let something:Option<Result<Result<i32, String>, String>> = Some(Ok(Ok(3)));

println!("something:{}", something.unwrap().unwrap().unwrap());

わずか1行で書けていますね。

OptionResultのchainをunwrap()なしでシンプルに書く方法はないでしょうか?Chainしたいですよね。

?演算子と、anyhowを使う

?演算子は Resultの後ろつけます。値がOkなら値を返し、値がErrならErrをReturnします。

Returnするので関数の戻り値の型にあわせる必要があります。

Rustは戻り値の型を明示的に指定する必要がありますが、Rust標準ではこの記述が難しいです。

この課題にはanyhowを使えます。anyhowは様々なErrorの抽象型のように使えるものです。

以下のように、異なるResultの型の違いをanyhow::Resultは吸収できます。

fn handle_many_error() -> anyhow::Result<i32>{

    let s = std::fs::read_to_string("./test.txt")?; // エラーだとstd::io::Result<String>を返す

    Ok(s.parse()?) // エラーだとstd::num::ParseIntErrorを返す
    
    //以下のようにも書ける
    //s.parse().map_err(anyhow::Error::msg)
}

これでResultは何とかできそうです。

しかし、Rustでは?演算子は Optionに使えません。これもanyhowが役立ちます。

use anyhow::Context as _;

//Contextが衝突しないなら、以下でもいい
use anyhow::Context

これを書くと、既存のOptioncontext()が生えます。
context()Optionanyhow::Resultに変換できます。

let hi = Some(3);
hi.context("nya"); //anyhow::ResultのOk(3)になる。hiがNoneだったらErr("nya")になる。

//Optionなので?演算子が使えるようになる
let value = hi.context("nya")?

これと?演算子を組み合わせると、matchが入れ子になっていたケースは以下のように書けます。

use anyhow::Context as _;
use anyhow::Result;

fn do_something() -> anyhow::Result<i32> {

    let something:Option<Result<Result<i32>>> = Some(Ok(Ok(3)));

    something
        .context("1st Option is None")?
        .context("error in 1st Result:")?
        .context("error in 2nd Result:")
}

fn main() {
    match do_something(){
        Ok(i)=>{
            println!("something:{}", i);
        }
        Err(e)=>{
            eprintln!("error:{:#}", e); // Resultにcontextを使う場合は、{}ではなく{:#}でないと内側のエラーが表示されない
        }
    }
}

この例のsomething.context()な処理部は、元々のResultErrを返すだけでいいなら以下のよう書けます。

//??は連続で利用できる
something.context("1st Option is None")??

context()やanyhowがなくてもOption:ok_or()Optionstd::result::Resultに変換することはできます。しかし、その場合は?演算子で返す型の指定が難しくなります。

そこで、anyhow::Resultを使うことで、?演算子が使いやすくなり、結果シンプルな記述ができるようになります。

最後に

「Rustは安全」と言われつつも危険な使い方があり、それを避けるコーディング方法を学ぶ必要があります。

unwrapの使い方を考えて、Panicしないプログラムを書いていきましょう。

Rustはいいぞ。もっとやれ。

26
16
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
26
16