LoginSignup
521
361

More than 1 year has passed since last update.

Rust のエラーハンドリングはシンタックスシュガーが豊富で完全に初見殺しなので自信を持って使えるように整理してみたら完全に理解した

Last updated at Posted at 2019-03-15

はじめに

  • 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 も列挙型なので matchif 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_thenmap と同様に 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

  • okResult<T, E>Option<T> に変換します
  • errResult<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_thenmap と同様に 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 ならデフォルト値を受け取るとか、語感的になんとなく判別できるので、半分くらいおぼえておいて困ったらドキュメントを見る感じでなんとかなる気がします。
521
361
4

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
521
361