###### tags: `Rust` `compile` `条件付き` `エラーハンドリング` # Better Rust 2 # #[cfg()] 条件付きコンパイルアトリビュート フラグに合わせてコンパイルする # エラーハンドリング プログラムを書く上でエラーの対処は重要 Rustでは、エラーの対処が冗長で面倒なものにならないような仕組みが ある。 sum type (直和型) と コンビネータ から、エラーハンドルを改善する動機を学ぶ。 ## エラー解決の基礎 ある処理が成功したか失敗したかを場合分けに基づいて判断する。 エラーハンドリングを使いやすくするには、合成可能(コンパイルできる)なコードを保ったまま 明示的な場合分け回数を以下に減らすかが重要。 コード合成可能性 = 想定外のときpanic!する可能性を減らす。 panicすると、多くの場合はプログラムがクラッシュする。 ## unwrap() メソッド Option\<T> と Result\<T> に実装されているメソッド。 .unwrap() の意味は、「値が取得できているならその値を返し、ないならpanic!して」 - Option\<T> : 値が不在の可能性。Some(T) と None の Variant を持つ - Result\<T, E> : 実行結果。Some(T) と Err(E) のVariantを持つ どちらのenum型も、正しい値が取得できないかもしれないという可能性を持つ ## Optionを返す関数のmatch fn() -> Option\<T> 型の関数のエラーハンドリング ```rust= fn main() { let file_name = "foobar.rs"; match find(file_name, '.') { None => println!("No file extension found."), Some(i) => println!("File extension: {}", &file_name[i+1..]), } } ``` match キーワードを使って、 None と Some(i) のパターンマッチングで値を取得して使用する。 ## unwrap() を使うときの場合分け 実際のところ、Option を処理するには match による場合分けを行うしかない。 unwrap() 内で行っていることはまさにそれであり、 - 値が取得できれば値を返す - なければpanic!する ということを行っているのである。 定義はかんたんなので、自分でOption型の unwarp() メソッドを定義し直すことで Noneの場合の処理をカスタマイズすることができる。 ```rust= enum Option<T> { None, Some(T), } impl<T> Option<T> { fn unwrap(self) -> T { match self { Option::Some(val) => val, Option::None => panic!("called `Option::unwrap()` on a `None` value"), } } } ``` デフォルトの panic! が実装されているunwrap() は、合成可能ではない(プログラムがクラッシュする) 陶器店の中の雄牛 ## Option\<T> 型の値を合成する 関数などの戻り値にするとき - Some(T) を返す - None を返す これだけでいい ### 例.指定したファイル名の拡張子を取り出す関数 プログラムからファイルを読み込むとき、拡張子を取り出すのはままあること なので、標準ライブラリで extension() として関数化されている ```rust= fn extension_explicit(file_name: &str) -> Option<&str> { match find(file_name, '.') { None => None, Some(i) => Some(&file_name[i+1..]), } } ``` ファイル名内の最初の . 以降 の文字列のスライスをSome()でラップして返す 拡張子がない可能性を考慮してNoneも返す ## コンビネータ Option型と組み合わせて使いましょうね、という Option型構造体のメソッドとして実装されている関数 ### option.map(arg, fn) mapとは : map関数。値と関数を引数にして、関数を適用した値を返す。 イテレータなどでよく使う ```rust= fn map<F, T, A>(option: Option<T>, f: F) -> Option<A> where F: FnOnce(T) -> A { match option { None => None, Some(value) => Some(f(value)), } } ``` これはOption\<T>型のメソッドとして定義されている。 しかし、戻り値はSome()でラップされ直すので使いづらい。 ## option.and_then(fn) option型の中身を場合分けして、 値があるときは引数の関数を適用した値を返す。 option.map() のSome() で再ラップしないバージョン。 例.ファイルパスからファイルを見つけて、ファイル名から拡張子を抜き出す。 ```rust= fn file_path_ext(file_path: &str) -> Option<&str> { file_name(file_path).and_then(extension) } ``` # Result\<T, E> 型 標準ライブラリで定義される。 Optionの豪華版で、Err\<E>のように失敗時の返り値をつけられる。 値の取得に失敗する場合などに、なぜ失敗したのかを説明するために用いる。 Option型は、Result型を特殊化したもの。 Err(E) が () 空タプルになったもの ```rust= type Option<T> = Result<T, ()>; ``` Variantは以下の2つ - Ok(T) - Err(E) Option型と同様に、 unwrap()が実装されている ## 文字列を整数値にパース(変換) Rustのstdを使うと、かんたんにstrをintに変換できる。 しかし、 - 空文字 - 整数値以外が含まれてる といった理由でパースが失敗する可能性もある Result\<T, E>を使うことでエラーハンドリングができる ```rust= impl str { fn parse<F: FromStr>(&self) -> Result<F, F::Err>; } ``` &str 型は、 .parse() メソッドを実装している。 パースできる型は、ジェネリックにより FromStr トレイトを実装している型のみ FromStrトレイトの実装を見ると、関連型にParseIntError がある。 このエラーが発生する可能性のある関数では、Resultのジェネリックパラメータに これを使おう ```rust= use std::num::ParseIntError; fn double_number(number_str: &str) -> Result<i32, ParseIntError> { number_str.parse::<i32>().map(|n| 2 * n) } fn main() { match double_number("10") { Ok(n) => assert_eq!(n, 20), Err(err) => println!("Error: {:?}", err), } } ``` Result型もOption型と同様に、 map(), and_then() コンビネータメソッドを実装している。 Option型との違いとして、Err(E) のみに影響をあたえる .map_err() や or_else() がある。 ## Result型のイディオム ### ネームスペースにおいて、Result型が返すErrの種類を固定する ```rust= use std::num::ParseIntError; use std::result; type Result<T> = result::Result<T, ParseIntError>; fn double_number(number_str: &str) -> Result<i32> { unimplemented!(); } ``` type キーワードを用いた型エイリアスにより、 パラメータ1つの Result\<T>を定義 ParseIntErrorが大量に使われる場合など、タイピング量をへらすことができる ## OptionをResultに変換 .ok_or(option, err) コンビネータ - Some(T) のときは Ok(T) を返す - None のときは Err(err) にして返す ```rust= fn ok_or<T, E>(option: Option<T>, err: E) -> Result<T, E> { match option { Some(val) => Ok(val), None => Err(err), } } ``` # stdライブラリによるエラー処理 stdで定義されている エラーハンドリングに欠かせないトレイト2種類 - Error トレイト - From トレイト ## Errorトレイト Debug トレイト と Display トレイトを継承している。 - Display .fmt() : println!マクロで"{}"のreplacementに表示する文字列を自動実装 - Debug .fmt() : 上の"{:?}"バージョン 上記に加えて、 - description() : エラー原因メッセージを返す - cause() : 一段下のエラーを返す。(あれば) cause() メソッドが Errorを入れ子で返すので、複数エラーに起因する場合でもエラーを追える ```rust= use std::fmt::{Debug, Display}; trait Error: Debug + Display { /// エラーの簡単な説明 fn description(&self) -> &str; /// このエラーの一段下のレベルの原因(もしあれば) fn cause(&self) -> Option<&Error> { None } } ``` ## From トレイト std::convert::From ある値を別の値に変換するための from(T)->Self を実装している ```rust= trait From<T> { fn from(T) -> Self; } ``` 驚くほど単純 使用例 ```rust= let string: String = From::from("foo"); let bytes: Vec<u8> = From::from("foo"); let cow: ::std::borrow::Cow<str> = From::from("foo"); ``` ## try! マクロ 本当のtry!マクロ ```rust= macro_rules! try { ($e:expr) => (match $e { Ok(val) => val, Err(err) => return Err(::std::convert::From::from(err)), }); } ``` try!(expr) の expr(式) を評価して、Ok() なら val, Err(err) なら Err(err) エラーが起きたときだけ Err() でラップして返す さらに、Err(err) の内部の値は、自動的に型変換される(異なるエラー型であっても、同一のエラー型になる) # エラーの章のまとめ 長いので、章の一番下のまとめを読むと良い