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による基本的なエラー処理