はじめに
- Rust の Result や Option を使ったエラーハンドリングは機能が豊富なので便利な半面、初心者にとっては難しく感じられてしまいます。
- とはいえよく見るとシンタックスシュガーが豊富なだけですごく難しいということはなかったので、自分の理解のためにも整理してまとめてみました。
Result とは
- Result は失敗する可能性のある関数の返り値のために用意された列挙型で、関数が成功した場合の返り値のバリアントと失敗した場合の返り値のバリアントを提供します。
enum Result<T, E> {
Ok(T), // 成功
Err(E), // 失敗
}
- Result は列挙型なので
match
で評価して利用します。 -
if let
を使うと成功した場合の処理だけを記述することができます。
fn main() {
let results = [Ok(100), Err("oops!")];
for r in results.iter() {
println!("Result: {:?}", r);
let double = match r {
Ok(v) => v * 2,
Err(_) => 0,
};
println!("match result: {}", double);
if let Ok(v) = r {
println!("if let result: {}", v);
}
println!("")
}
}
標準出力
Result: Ok(100)
match result: 200
if let result: 100
Result: Err("oops!")
match result: 0
Option とは
Option は空になる可能性のある値を表現するために用意された列挙型で、空の場合のバリアントと値のある場合のバリアントを提供します。
enum Option<T> {
None, // 値が空
Some(T), // 値がある
}
- Option も列挙型なので
match
やif let
で評価して利用します。
fn main() {
let options = [Some(-1), None];
for o in options.iter() {
println!("Option: {:?}", o);
let double = match o {
Some(v) => v * 2,
None => 0
};
println!("match result: {}", double); //=> -2
if let Some(v) = o {
println!("if let result: {}", v);
}
println!("")
}
}
標準出力
Option: Some(-1)
match result: -2
if let result: -1
Option: None
match result: 0
unwrap メソッド
- Rust のエラーハンドリングで一番目につまづくのが unwrap メソッドかなと思います。
- 便利なのでサンプルコードなんかでよく使われていますが、ちゃんと Result の使い方を知る前に出てきちゃったりするので初心者的にはおまじないに見てしまっていてあまり良くないです。
解説
- Result.unwrap メソッドは Ok なら中の値を返し、Err なら panic を起こします。
fn main() {
let result: Result<i32, String> = Ok(2);
println!("{:?}", result.unwrap()); //=> 2
let result: Result<i32, String> = Err("error".to_string());
println!("{:?}", result.unwrap()); //=> panic!
}
- Option も unwrap メソッドを持っており、Result と同じように Some なら中の値を返し、None なら panic を起こします
fn main() {
let option: Option<i32> = Some(2);
println!("{:?}", option.unwrap()); //=> 2
let option: Option<i32> = None;
println!("{:?}", option.unwrap()); //=> panic
}
- このように unwrap を使うと match や if let などを使わすに一行で手軽に Ok の中の値を取得できるので便利です
- が、失敗すると panic を起こすので、実際に利用する際にはチェックしてから使用するか後述する別のメソッドを使うかと思います。
panic を起こさない unwrap の類似メソッド
- unwrap の類似メソッドで panic の代わりに任意のデフォルト値を指定することができる
unwrap_or
と panic の代わりにクロージャを実行した値を返すunwrap_or_else
があります。
fn main() {
let result: Result<i32, String> = Err("error".to_string());
println!("{:?}", result.unwrap_or(10)); //=> 10
let result: Result<i32, String> = Err("error".to_string());
println!("{:?}", result.unwrap_or_else(|| 20)); //=> 20
let option: Option<i32> = None;
println!("{:?}", option.unwrap_or(100)); //=> 100
let option: Option<i32> = None;
println!("{:?}", option.unwrap_or_else(|| 200)); //=> 200
}
? 演算子
- Rust のエラーハンドリングで二番目につまづくのが ? 演算子だと思います。
- これもサンプルコードに気軽に出てきますが説明が足りないのでおまじないになってしまっていますし、類似の概念があまり他の言語にないのでなかなか手強いです。
解説
- ? 演算子は Result を返す関数の中で利用されます。
- ? 演算子は Result 型の値の後ろにつけて利用され、値が Ok なら中の値を返し、値が Err なら即座に値を return します。
- わかりやすくするため
let left = right?;
を擬似コードにするとこのようになります。right は Result 型の変数です。
let left: i32;
// is_ok() は right が Ok なら ture を返します
if right.is_ok() {
left = right.unwrap();
} else {
return right;
}
- ? 演算子が便利なのは Result を受け取ったときにエラーならすぐに処理を中止して return したいというときで、よくあるこんな処理を少ない記述量で手軽に実装することができます。
例
- 実際に ? 演算子を使っているコードをみてみましょう。
- Result を返す
double
関数のなかで同じく Result を返すodd
が呼び出され ? 演算子を使ってエラーがハンドリングされています。
fn main() {
let odd = |n| -> Result<i32, String> {
if n % 2 == 1 {
Ok(n)
} else {
Err(format!("{} is not odd", n))
}
};
let double = |n| -> Result<i32, String> {
let n = odd(n)?; // odd が Err ならすぐに return する
return Ok(n * 2);
};
for n in 0 .. 4 {
println!("number: {}", n);
println!("double result: {:?}\n", double(n));
}
}
標準出力
number: 0
double result: Err("0 is not odd")
number: 1
double result: Ok(2)
number: 2
double result: Err("2 is not odd")
number: 3
double result: Ok(6)
- Rust と同じようにエラーを返り値でハンドリングする Go を利用したことがあるとわかると思いますが
if err != nil { return err }
のようなコードを延々と書く必要がなくなるというのが ? 演算子のいいところですね。(Go のほうでもシンタックスシュガーを用意しようとしているらしいですが)
Option でも
- ? 演算子は Option でも同様に利用することができて、Some なら中の値を返し None ならすぐに return します。
fn main() {
let double = |n: Option<i32>| -> Option<i32> {
println!("Option: {:?}", n);
let m = n?;
return Some(m * 2);
};
println!("{:?}\n", double(Some(100)));
println!("{:?}", double(None));
}
標準出力
Option: Some(100)
Some(200)
Option: None
None
Result のいろいろな操作
成功した時だけ中の値を加工する map
-
map
はクロージャを受け取り Result の値がOk
の時だけクロージャを実行して中の値を加工します。値がErr
なら何もしません。
fn main() {
let results = [Ok(100), Err("Error")];
println!("{:?}", results[0].map(|n| n * 2)); //=> Ok(200)
println!("{:?}", results[1].map(|n| n * 2)); //=> Err("Error")
}
- 失敗した時だけ中の値を加工したい場合は
map_err
メソッドが用意されています。
成功した時だけ処理を実行する and_then
-
and_then
はmap
と同様に Result の値が Ok の時だけクロージャを実行します。 -
map
が Ok の中の値を加工するのに対して、and_then
はその名の通りに任意の処理を実行して新たな Result を作成して返却します。 -
and_then
の入力値と出力値で Result の型が変わる様な使い方もできます。
fn main() {
let results = [Ok(100), Err("Error")];
let f = |n| Ok(n * 2);
println!("{:?}", results[0].and_then(f)); //=> Ok(200)
println!("{:?}", results[1].and_then(f)); //=> Err("Error")
let f = |n| Ok(format!("number is {}", n));
println!("{:?}", results[0].and_then(f)); //=> Ok("number is 100")
}
- 失敗した時だけ処理を実行したい場合は
or_else
メソッドが用意されています。
Result から Option に変換する ok
err
-
ok
はResult<T, E>
をOption<T>
に変換します -
err
はResult<T, E>
をOption<E>
に変換します
fn main() {
let results = [Ok(100), Err("err")];
println!("{:?}", results[0].ok()); //=> Some(100)
println!("{:?}", results[0].err()); //=> None
println!("{:?}", results[1].ok()); //=> None
println!("{:?}", results[1].err()); //=> Some("err")
}
中の値を参照にする as_ref
as_mut
-
as_ref
は&Result<T, E>
をResult<&T, &E>
に変換します -
as_mut
は&mut Result<T, E>
をResult<&mut T, &mut E>
に変換します
Result 同士の和や積を求める and
or
-
and
メソッドやor
メソッドを使うと boolean のように Result 同士の和や積を求めることができる
fn main() {
let results = [Ok(1), Ok(2), Err("E1"), Err("E2")];
println!("{:?}", results[0].and(results[1])); //=> Ok && Ok = Ok(2)
println!("{:?}", results[0].and(results[2])); //=> Ok && Err = Err("E1")
println!("{:?}", results[2].and(results[1])); //=> Err && Ok = Err("E1")
println!("{:?}", results[2].and(results[3])); //=> Err && Err = Err("E1")
println!("{:?}", results[0].or(results[1])); //=> Ok || Ok = Ok(1)
println!("{:?}", results[0].or(results[2])); //=> Ok || Err = Ok(1)
println!("{:?}", results[2].or(results[1])); //=> Err || Ok = Ok(2)
println!("{:?}", results[2].or(results[3])); //=> Err || Err = Err("E2")
}
その他のメソッド
- 紹介できていないものもあるので知らないメソッドが出てきたら公式ドキュメントを読むのが良いと思います。
Option のいろいろな操作
値がある時だけ中の値を加工する map
-
Option.map
は値があればクロージャを実行して中の値を加工し、空なら何もしません。
fn main() {
let options = [Some(100), None];
println!("{:?}", options[0].map(|n| n * 2)); //=> Some(200)
println!("{:?}", options[1].map(|n| n * 2)); //=> None
}
値がある時だけ処理を実行する and_then
-
and_then
はmap
と同様に Option の値が Some の時だけクロージャを実行します。 -
map
が Some の中の値を加工するのに対して、and_then
はその名の通りに任意の処理を実行して新たな Option を作成して返却します。 -
and_then
の入力値と出力値で Option の型が変わる様な使い方もできます。
fn main() {
let options = [Some(100), None];
let f = |n| Some(n * 2);
println!("{:?}", options[0].and_then(f)); //=> Some(200)
println!("{:?}", options[1].and_then(f)); //=> Err("Error")
let f = |n| Some(format!("number is {}", n));
println!("{:?}", options[0].and_then(f)); //=> Some("number is 100")
}
- 値が空の時だけ処理を実行したい場合は
or_else
メソッドが用意されています。
Option から Result に変換する ok_or
-
ok_or<E>
はOption<T>
をResult<T, E>
に変換します
fn main() {
let options = [Some(100), None];
println!("{:?}", options[0].ok_or("err")); //=> Ok(100)
println!("{:?}", options[1].ok_or("err")); //=> Err("err")
}
Option 同士の和や積を求める and
or
-
and
メソッドやor
メソッドを使うと boolean のように Option 同士の和や積を求めることができる
fn main() {
let options = [Some(100), Some(200), None, None];
println!("{:?}", options[0].and(options[1])); //=> Some && Some = Some(200)
println!("{:?}", options[0].and(options[2])); //=> Some && None = None
println!("{:?}", options[2].and(options[1])); //=> None && Some = None
println!("{:?}", options[2].and(options[3])); //=> None && None = None
println!("{:?}", options[0].or(options[1])); //=> Some || Some = Some(100)
println!("{:?}", options[0].or(options[2])); //=> Some || None = Some(100)
println!("{:?}", options[2].or(options[1])); //=> None || Some = Some(200)
println!("{:?}", options[2].or(options[3])); //=> None || None = None
}
その他のメソッド
- 紹介できていないものもあるので知らないメソッドが出てきたら公式ドキュメントを読むのが良いと思います。
おわりに
- Result も Option もシンタックスシュガーが多くて初見だと不安になってしまいますが、
*_then
ならポジティブな値のときにクロージャを実行するとか*_else
ならその逆だとか*_or
ならデフォルト値を受け取るとか、語感的になんとなく判別できるので、半分くらいおぼえておいて困ったらドキュメントを見る感じでなんとかなる気がします。