Rustの?演算子でエラーやNoneを呼び出し元に委譲
Rustでは、?演算子でResult<T, E>のエラーErrやOption<T>のNoneを関数内から呼び出し元に委譲(propagate)できる。
?演算子によって、エラー処理を個別の関数で行わずに呼び出し元に任せられるようになる。
?演算子でResult<T, E>のエラーを委譲
まず、Result<T, E>のエラーErrを委譲する場合について説明する。
?演算子が使えるのはResult<T, E>を返す関数内部のみ
?演算子はResult<T, E>と評価される式(Result<T, E>そのものも含む)の末尾に付けられる。
例えばxがResult<T, E>の場合、x?は以下と等価。
match x {
Ok(v) => v,
Err(e) => return Err(From::from(e)),
}
成功したときはその成功値と評価され、失敗したときはエラーをreturn(早期リターン)する。
エラーをreturnするので、?演算子を使えるのはResult<T, E>を返す関数の内部のみ。さらに、?でreturnするエラーの型が返り値のエラーの型にFrom::fromで変換できなければならない(もちろん一致していればそれでもよい)。
具体例
文字列を数値に変換
文字列を数値に変換(パース)して何らかの処理を行う関数を例とする。ここでは単に2倍して返す。
一番簡単なのはunwrapしてしまうことだが、数値に変換できない文字列を渡すと実行時にパニックになってしまう。
fn parse_with_unwrap(s: &str) -> i32 {
let i = s.parse::<i32>().unwrap();
i * 2
}
assert_eq!(parse_with_unwrap("10"), 20);
// let x = parse_with_unwrap("abc");
// thread 'main' panicked at 'called `Result::unwrap()` ...
matchを使う例。parseが返すエラーに合わせて返り値のエラーの型を指定する。From::fromは不要なので省略している。
fn parse_with_match(s: &str) -> Result<i32, std::num::ParseIntError> {
let i = match s.parse::<i32>() {
Ok(i) => i,
Err(e) => return Err(e),
};
Ok(i * 2)
}
assert_eq!(parse_with_match("10").unwrap(), 20);
assert!(parse_with_match("abc").is_err());
?演算子を使うともっとシンプルに書ける。
fn parse_with_question(s: &str) -> Result<i32, std::num::ParseIntError> {
let i = s.parse::<i32>()?;
Ok(i * 2)
}
assert_eq!(parse_with_question("10").unwrap(), 20);
assert!(parse_with_question("abc").is_err());
ここでは、簡単のため、返り値に対してunwrapで値を取り出したりis_errでエラーであることを確認したりしているが、実際のプログラムでは適切なエラー処理を行えばよい。以降の例も同じ。
テキストファイル読み込み
std::fs::read_to_stringでテキストファイルの中身を読み込んで処理する関数を例とする。read_to_stringはResult<String, std::io::Error>を返す。
なお、Result<T, std::io::Error>はstd::io::Result<T>と省略して書けるが、以下の例では省略していない。
?演算子を使う例のみ示す。
use std::fs;
fn read_with_question(path: &str) -> Result<(), std::io::Error> {
let s: String = fs::read_to_string(path)?;
// do something
Ok(())
}
let non_existent_path = "path/to/non_existent_file";
assert!(read_with_question(non_existent_path).is_err());
関数として値を返す必要がなくても、返り値の型をResult<(), ...>としたうえでOk(())を返す必要があるので注意。
例は省略するが、存在するパスを指定してread_to_stringが成功した場合は、関数内の変数sに読み込まれた文字列Stringが束縛される。
?演算子でOption<T>のNoneを委譲
?演算子はOption<T>のNoneの委譲にも使われる。
?演算子が使えるのはOption<T>を返す関数内部のみ
?演算子はOption<T>と評価される式(Option<T>そのものも含む)の末尾に付けられる。
例えばxがOption<T>の場合、x?は以下と等価。
match x {
Some(v) => v,
None => return None,
}
xがSomeの場合(値を持つ場合)はその値と評価され、Noneの場合はNoneをreturn(早期リターン)する。
Noneをreturnするので、?演算子を使えるのはOption<T>を返す関数の内部のみ。
具体例
以下の関数を例とする。あくまでも例で特に意味のある処理ではない。
fn with_question(v: &mut Vec<i32>) -> Option<String> {
let i = v.pop()?;
let i = i.checked_add(100)?;
Some(i.to_string())
}
整数のベクタVec<i32>の可変参照を受け取り、popで末尾の要素を取り出し、checked_addで100を加え、to_stringで文字列Stringに変換している。
popはベクタが空のときNoneを返し、checked_addはオーバーフローしたときにNoneを返す。
実行結果は以下の通り。pop, checked_addそれぞれの場合でNoneが返されている。
let mut v: Vec<i32> = vec![0, 10, 20];
assert_eq!(with_question(&mut v).unwrap(), "120");
let mut v: Vec<i32> = vec![];
assert_eq!(with_question(&mut v), None);
let mut v: Vec<i32> = vec![i32::MAX];
assert_eq!(with_question(&mut v), None);
なお、例の関数は以下のように続けて書くこと可能。?演算子を使うことで、エラーやNoneに対応しながら簡潔にコードを書けるようになる。
fn _with_question(v: &mut Vec<i32>) -> Option<String> {
Some(v.pop()?.checked_add(100)?.to_string())
}
複数のエラー型に対応
これまでの例のように、関数内で発生するのがエラー型が同じResultだけの場合やOptionだけの場合は簡単だが、異なる複数のエラー型のResultや、ResultとOptionの組み合わせに対応するのは大変。
エラー型を独自に定義する方法やBoxを使う方法について、Rust By Exampleの以下の章で例を挙げて説明されている。
anyhowを使うと簡単。詳細は以下の記事を参照。
- 関連記事: Rust, anyhowによる基本的なエラー処理