はじめに
Rustはクラウド業界から組込み業界まで幅広く使われるようになりました。
こんなにコンパイラに怒られまくるマゾい言語が流行っているなんて、最高です。お前ら。
私はお仕事でRustを使っており、Rust布教をしています。
Rustは安全と言われていますが、すぐにPanicするプログラムに溢れています。サンプルコードをコピペすると簡単にPanicするプログラムが出来上がります。なぜでしょうか?
この記事ではunwrap警察として、世の中のRustからPanicを減らす方法について書いてみます。
アンラッパー
Optional型がある言語(SwiftとかJava8とか)から来た人を除いて、Rustのtry/catchではなく、OptionやResultでエラー処理をするスタイルに違和感を覚えた人も多いでしょう。
Option::unwrap()
まずはRust基本テクニックのOption::unwrap()
です。これはOption
がSome
なら値を返して、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()
と戦わないといけないケース
Option
やResult
を正しく皮むきし、エラー処理が必要なケースです。
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);
}
//エラー処理してないよ
ただ、Option
やResult
が入れ子になっている場合は大変です。
この例のように、入れ子で変数を定義することはなくても、処理の途中で入れ子になるケースがあります。
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行で書けていますね。
Option
やResult
の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
これを書くと、既存のOption
にcontext()
が生えます。
context()
はOption
をanyhow::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()
な処理部は、元々のResult
のErr
を返すだけでいいなら以下のよう書けます。
//??は連続で利用できる
something.context("1st Option is None")??
context()
やanyhowがなくてもOption:ok_or()
でOption
をstd::result::Result
に変換することはできます。しかし、その場合は?演算子で返す型の指定が難しくなります。
そこで、anyhow::Result
を使うことで、?演算子が使いやすくなり、結果シンプルな記述ができるようになります。
最後に
「Rustは安全」と言われつつも危険な使い方があり、それを避けるコーディング方法を学ぶ必要があります。
unwrapの使い方を考えて、Panicしないプログラムを書いていきましょう。
Rustはいいぞ。もっとやれ。