Rust を始める時に少しだけ読み書きしやすくなる Result と Option の話
2021年11月29日 月曜日
CONTENTS
自己紹介
IIJ で社内向けの運用システムやサービス共通の基盤の開発をしています。
最近は Golang, Dart (Flutter) , Rust あたりを主に業務として使用しています。
今回仕事で Rust のコードを書いた経験から社内向けに記事を作成したところ、こちらに掲載しないかとのお誘いをいただきました。
Rust の簡単な紹介
インストール
https://www.rust-lang.org/tools/install
チュートリアルなど
https://www.rust-lang.org/learn
基本の流れ
- cargo new <name> で新しいプロジェクトのディレクトリと基本ファイルを作り
- Cargo.toml に依存や main ファイルの指定などを書き
- src/ 以下にソースコードを配置し
- cargo run で実行 / cargo build でバイナリビルド
検索で見つけた便利そうな外部ライブラリを使う
例: 「時間情報を扱いたかったので調べたら chrono っていうのがよさそうだった」
chrono の GitHub 画面の README
- crates.io が Rust のライブラリの集合サイト
- docs.rs がライブラリ等のドキュメント集合サイト
crates.io の画面
この画面だと chrono の最新バージョンは 0.4 なので Cargo.toml の依存のところに追加する。
: [dependencies] chrono = "0.4" :
あとはソースコード中で import (use) して使用、 cargo run / build 時に自動で依存関係を引っ張ってきてくれる。
Result と Option について
Result によるエラー処理
Python などの try-except ではなく、 Golang の error 処理に近い。以下比較。
- Golang では error interface を満たすものを関数などから返すことにより、その中でエラーが起こったことを知って適切な処理をする。
func main() { v, err := someMethod() if err != nil { // 適切なエラー処理 } } func someMethod() (String, error) { // 省略 }
- Rust では Result という値を保持できる enum が存在し、それを返すのが習わし。
その Result enum の値で分岐させ、関数などからエラーが返ってきたことを知って適切な処理をする。fn main() -> Result<(), Box<dyn Error> { let v = match some_method() { Ok(v) => v, Err(e) => // 適切なエラー処理 } Ok(()) } fn some_method() -> Result<String, SomeErrorType> { // 省略 } // **参考** 標準ライブラリ内の Result の定義 pub enum Result<T, E> { Ok(T), Err(E), }
Option による Null 的(値が存在しない)なものの扱い
Rust では値が存在しない可能性があるものには Option という enum を使用するのが習わし。使い方としては Result ほぼ同じ。
fn main() -> Result<(), Box<dyn Error> { let v = match get_item() { Some(v) => v, None => // 値がなかった時の処理 } Ok(()) } fn get_item() -> Option<String> { // 省略 } // **参考** 標準ライブラリ内の Option の定義 pub enum Option<T> { None, Some(T), }
Result と Option の if let での分岐
一応 if let みたいな構文があり、 Result が Ok だったり Option が Some だった場合などに分岐できる if 文も書ける。ただ、自分が慣れた書き方だと Err や None だった場合の処理を if の中に入れ、それ以降で Ok や Some の中の値を使いたい場合が多いのでそこまで使いどころがない…。
// Option の if let // この場合 v のスコープは if の中だけなのでちょっと個人的には使い勝手微妙に感じる。 // どちらかというと None にマッチさせたいが、そうすると結局その後に Some の中の値を取り出さないといけない。 // それだったら最初から match 書いたほうが分かりやすくていいと思う。 if let Some(v) = value { println!("value = {}", v); } // Result の if let if let Ok(v) = value { println!("value = {}", v); }
Result も Option も特別なものではないという話
上記で Result と Option というのも同じ処理の仕方ができることが分かったと思うが、これは Result も Option もどちらも Rust 上で特別な意味を持つ予約された値ではなくあくまで標準ライブラリに定義された enum であるため。
つまり上記で紹介した match や if let での分岐も Result , Option にしか使えないものではなく、もっと幅広く使える Rust での一般的な文法となる。
unwrap メソッドはなるべく使わないほうがいいんじゃないかという話
Result にも Option にも unwrap というメソッドが生えており、これは Ok/Some の中身をそのまま取り出せる代わりに Err/None の場合は panic する。
結構な頻度でサンプルコードに存在するが、プロダクションレベルでは使わないほうが良いと思う。
Result の場合回復不可能なエラーしか起こらないとき (設定ファイルが見つからない、指定されたアドレスで listen できないなど) 、 Option の場合は確実に値が存在するときくらいではないか。
// Option の例 let v = get_item().unwrap(); // get_item から None が返ってきた場合は panic println!("value = {}", v); fn get_item() -> Option<String> { // 省略 }
まとめ
知っていると少しだけ Rust が読み書きしやすくなる Result とOption の話をさせていただきました。
記事中でも太字で書いたことの繰り返しになってしまいますが、 Result も Option もただの定義された enum であり Rust 上で特別なヨクワカラナイナニカではありません。
やろうと思えば自作コード中で定義した別の enum で代用することだって可能です。
これをわかっていれば Result / Option を使って分岐しているコードを読んでも「ああ、 enum の値を見てるんだな」と思うだけでだいぶ理解がしやすくなると思います。
Rust はただでさえ所有権やスコープなどで少しとっつきにくい印象があると思いますので、この記事で少しでも Rust を触る時の心理的障壁を少なく感じてもらえたら嬉しいです。