1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Qiitanがほしい人の一人アドカレAdvent Calendar 2024

Day 18

Rust未経験者がRust100-exercises をやってみた話 (Enumとエラーハンドリング)

Posted at

よみとばしポイント

どうも限界派遣SESのnikawamikanです。
ひとりアドカレ18日目の今日もQiitan獲得のために頑張ります。
なお1日遅刻していますが、睡魔には勝てなかったと言っておきます。

先日の記事の続きです。

前回からの続き

前回は基本構文とトレイトについて話しました。
どちらもRustの特徴的な部分で、他の言語とは違う特徴がいくつもあります。

今回はEnumとエラーハンドリングについて話していきます。

Enumとエラーハンドリング

Enumは列挙型とも呼ばれ、複数の値をまとめて扱うための型です。
これ自体は他の言語でもある概念ですが、RustのEnumは他の言語とは違う特徴があります。

よくあるEnum

例えば、Eventごとに処理を分けるようなプログラムを書くときにEnumを使うことがあります。

enum Event {
    Start,
    Stop,
    Pause,
    Resume,
}

fn main() {
    let event = Event::Start;
    match event {
        Event::Start => println!("Start"),
        Event::Stop => println!("Stop"),
        Event::Pause => println!("Pause"),
        Event::Resume => println!("Resume"),
    }
}

これは他の言語でもよく見られるEnumの使い方です。

Enumに値を持たせる

RustのEnumは他の言語とは違い、Enumに値を持たせることが出来ます。

例えば、Start時に何秒後に処理を開始するかを持たせることが出来ます。

enum Event {
    Start(u32),
    Stop,
    Pause,
    Resume,
}

fn main() {
    let event = Event::Time(10);
    match event {
        Event::Start(time) => println!("Start after {} seconds", time),
        Event::Stop => println!("Stop"),
        Event::Pause => println!("Pause"),
        Event::Resume => println!("Resume"),
    }
}

このようにEnumに値を持たせることで、Eventごとに値を持たせることが出来ます。
なかなかおもしろいですよね。

これにより、Eventごとに処理を分けるだけでなく、Eventごとに値を持たせることが出来ます。

この機能、他の言語で使いたいなぁー。

エラーハンドリング

上記のEnumの特徴を活かして、RustではエラーハンドリングをEnumを使って行うことが出来ます。

そのEnumがResult型です。
OkとErrの2つの値を持ち、エラーが発生した場合はErrを返し、正常終了した場合はOkを返します。

fn divide(a: i32, b: i32) -> Result<i32, String> {
    if b == 0 {
        // Errの場合はエラーメッセージを返す
        return Err("division by zero".to_string());
    }
    // Okの場合は計算結果を返す
    Ok(a / b)
}

fn main() {
    // 0割り算によってエラーが発生する
    let result = divide(10, 0);
    match result {
        Ok(value) => println!("Result: {}", value),
        Err(message) => println!("Error: {}", message),
    }
}

このように、Result型を使うことでエラーハンドリングを行うことが出来ます。
またエラー時に返す値は任意の型であるため、Enumなどを定義してエラーの種類ごとに処理を分けることも出来ます。

use std::fmt::Display;

enum MyError {
    DivisionByZero,     // ゼロ除算
    NankaError(String), // なんかエラー
}

// Displayトレイトを実装することで、エラーメッセージを表示できるようにする
impl Display for MyError {
    fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
        match self {
            MyError::DivisionByZero => write!(f, "division by zero"),
            MyError::NankaError(nanka) => {
                write!(f, "なんかエラーになったらしいよ 原因 → {}", nanka)
            }
        }
    }
}

fn returns_error() -> Result<(), MyError> {
    // なんかエラーを返す
    Err(MyError::NankaError("なんかエラーになった".to_string()))
}

fn divide(a: i32, b: i32) -> Result<i32, MyError> {
    if b == 0 {
        Err(MyError::DivisionByZero)
    } else {
        Ok(a / b)
    }
}

fn main() {
    // エラーを返す関数を呼び出す
    let result = returns_error();
    // エラーが返ってきたら、エラーメッセージを表示する
    if let Err(e) = result {
        println!("Error: {}", e);
    }

    // ゼロ除算を行う関数を呼び出す
    let result = divide(1, 0);
    // エラーが返ってきたら、エラーメッセージを表示する
    if let Err(e) = result {
        println!("Error: {}", e);
    }
}

これで実行すると以下のようになります。

Error: なんかエラーになったらしいよ 原因 → なんかエラーになった
Error: division by zero

いいですよね。
型ごとに決まったエラーを返すことが出来るし、エラーの種類によっては値も一緒に返すことが出来ます。

Nullableな値

RustにはNullという概念はありません。

その代わりにOption型があり、Nullableな値を表すことが出来ます。
Option型はEnumであり、SomeとNoneの2つの値を持ちます。

fn divide(a: i32, b: i32) -> Option<i32> {
    // ゼロ除算の場合はNoneを返す
    if b == 0 {
        return None;
    }
    Some(a / b)
}

fn main() {
    let result = divide(10, 0);
    match result {
        Some(value) => println!("Result: {}", value),
        None => println!("Error: division by zero"),
    }
}

このようにOption型を使うことでNullableな値を表すことが出来ます。

なんていうかNullableな型というのを用意するのではなく、Rustの機能であるEnumで表現するのが個人的にすごい刺さっています。

この言語の設計思想のようなものが垣間見えているように感じます。

Option型は早期リターンに使える

Option型は?演算子と組み合わせることで、早期リターンを行うことが出来ます。

fn divide(a: i32, b: i32) -> Option<i32> {
    if b == 0 {
        return None;
    }
    Some(a / b)
}

fn all_divide(a: i32, b: i32, c: i32) -> Option<i32> {
    let result = divide(a, b)?;
    let result = divide(result, c)?;
    Some(result)
}

fn main() {
    // 最初の割り算で0割り算が発生する
    let result = all_divide(10, 0, 5);
    match result {
        Some(value) => println!("Result: {}", value),
        None => println!("Error: division by zero"),
    }
}

このように?演算子を使うことで、早期リターンを行うことが出来ます。
これはエラーハンドリングの際も利用できます。

fn divide(a: i32, b: i32) -> Result<i32, String> {
    if b == 0 {
        return Err("division by zero".to_string());
    }
    Ok(a / b)
}

fn all_divide(a: i32, b: i32, c: i32) -> Result<i32, String> {
    let result = divide(a, b)?;
    let result = divide(result, c)?;
    Ok(result)
}

fn main() {
    let result = all_divide(10, 0, 5);
    match result {
        Ok(value) => println!("Result: {}", value),
        Err(message) => println!("Error: {}", message),
    }
}

Rustには例外処理がないため、このように早期リターンを使ってエラーハンドリングを行うことが多いです。

例外処理がある言語では、多くの場合でキャッチするべき例外が多すぎて、どの例外をキャッチするべきか迷ったりしますが、Rustの構文だと何が返ってくるのかが明確なため、エラーハンドリングがしやすいです。

あれ?思ったより書きやすい言語なのでは?

エラーがないことを前提としてunwrapする

Rustではエラー型を返す関数の中で、エラーがないことを前提として処理を行うことが出来ます。

fn divide(a: i32, b: i32) -> Result<i32, String> {
    if b == 0 {
        return Err("division by zero".to_string());
    }
    Ok(a / b)
}

fn main() {
    let result = divide(10, 5).unwrap(); // エラーがないことを前提としてunwrapする
    println!("Result: {}", result);
}

これはunwrapというメソッドを使うことで、正常終了を前提として値を取り出すことが出来ます。
例えばdivide関数の第二引数にいれる前に0が入らないことを保証することが出来る場合、このようにエラーがないことを前提として処理を行うことが出来ます。

しかし、unwrapはエラーが発生した場合にパニックを起こすため、エラーが発生した際に継続する処理がない場合に使うべきです。

エラーがないことを前提としてexpectする

unwrapと似ていますが、expectはエラーが発生した際のパニックの文章を追記することができます。

fn divide(a: i32, b: i32) -> Result<i32, String> {
    if b == 0 {
        return Err("division by zero".to_string());
    }
    Ok(a / b)
}


fn main() {
    let result = divide(10, 0).expect("エラーが発生しました"); // エラーがないことを前提としてexpectする
    println!("Result: {}", result);
}
thread 'main' panicked at src/main.rs:9:32:
エラーが発生しました: "division by zero"
stack backtrace:
   0: rust_begin_unwind
             at /rustc/9b00956e56009bab2aa15d7bff10916599e3d6d6/library/std/src/panicking.rs:645:5
   1: core::panicking::panic_fmt
             at /rustc/9b00956e56009bab2aa15d7bff10916599e3d6d6/library/core/src/panicking.rs:72:14
   2: core::result::unwrap_failed
             at /rustc/9b00956e56009bab2aa15d7bff10916599e3d6d6/library/core/src/result.rs:1654:5
   3: core::result::Result<T,E>::expect
             at /rustc/9b00956e56009bab2aa15d7bff10916599e3d6d6/library/core/src/result.rs:1034:23
   4: bocchi::main
             at ./src/main.rs:9:18
   5: core::ops::function::FnOnce::call_once
             at /rustc/9b00956e56009bab2aa15d7bff10916599e3d6d6/library/core/src/ops/function.rs:250:5
note: Some details are 

エラーが発生しました: "division by zero"となっていますね。
メッセージをカスタムしておけばエラー箇所を特定するのが楽になるかもしれません。

まとめ

Enumとエラーハンドリングについて話しました。

他の言語と違うEnumの使い方ですが、個人的にはとても好きです。

また、今回はコレクションの話は他の言語とあまり差がないと感じたので割愛しています。
ただ、HashMapを利用する際に以下の記事でハッシュ関数を変更できるという話があり参考になったので紹介させてください。

それでは。次回は非同期処理について触れていこうと思います。

1
0
0

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
1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?