1
0
お題は不問!Qiita Engineer Festa 2024で記事投稿!
Qiita Engineer Festa20242024年7月17日まで開催中!

Rust 100 Ex 🏃【15/37】 Result型 ~Rust流エラーハンドリング術~

Last updated at Posted at 2024-07-17

前の記事

全記事一覧

100 Exercise To Learn Rust 演習第15回になります!

今回の関連ページ

[05_ticket_v2/06_fallibility] Rust用エラーハンドリング機構 Result型

問題はこちらです。

lib.rs
// TODO: Convert the `Ticket::new` method to return a `Result` instead of panicking.
//   Use `String` as the error type.

#[derive(Debug, PartialEq)]
struct Ticket {
    title: String,
    description: String,
    status: Status,
}

#[derive(Debug, PartialEq)]
enum Status {
    ToDo,
    InProgress { assigned_to: String },
    Done,
}

impl Ticket {
    pub fn new(title: String, description: String, status: Status) -> Ticket {
        if title.is_empty() {
            panic!("Title cannot be empty");
        }
        if title.len() > 50 {
            panic!("Title cannot be longer than 50 bytes");
        }
        if description.is_empty() {
            panic!("Description cannot be empty");
        }
        if description.len() > 500 {
            panic!("Description cannot be longer than 500 bytes");
        }

        Ticket {
            title,
            description,
            status,
        }
    }
}
テストを含めた全体
lib.rs
// TODO: Convert the `Ticket::new` method to return a `Result` instead of panicking.
//   Use `String` as the error type.

#[derive(Debug, PartialEq)]
struct Ticket {
    title: String,
    description: String,
    status: Status,
}

#[derive(Debug, PartialEq)]
enum Status {
    ToDo,
    InProgress { assigned_to: String },
    Done,
}

impl Ticket {
    pub fn new(title: String, description: String, status: Status) -> Ticket {
        if title.is_empty() {
            panic!("Title cannot be empty");
        }
        if title.len() > 50 {
            panic!("Title cannot be longer than 50 bytes");
        }
        if description.is_empty() {
            panic!("Description cannot be empty");
        }
        if description.len() > 500 {
            panic!("Description cannot be longer than 500 bytes");
        }

        Ticket {
            title,
            description,
            status,
        }
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use common::{overly_long_description, overly_long_title, valid_description, valid_title};

    #[test]
    fn title_cannot_be_empty() {
        let error = Ticket::new("".into(), valid_description(), Status::ToDo).unwrap_err();
        assert_eq!(error, "Title cannot be empty");
    }

    #[test]
    fn description_cannot_be_empty() {
        let error = Ticket::new(valid_title(), "".into(), Status::ToDo).unwrap_err();
        assert_eq!(error, "Description cannot be empty");
    }

    #[test]
    fn title_cannot_be_longer_than_fifty_chars() {
        let error =
            Ticket::new(overly_long_title(), valid_description(), Status::ToDo).unwrap_err();
        assert_eq!(error, "Title cannot be longer than 50 bytes");
    }

    #[test]
    fn description_cannot_be_longer_than_500_chars() {
        let error =
            Ticket::new(valid_title(), overly_long_description(), Status::ToDo).unwrap_err();
        assert_eq!(error, "Description cannot be longer than 500 bytes");
    }
}

ついに... 第5回 の懸念事項が解消されます!

解説

lib.rs
impl Ticket {
-     pub fn new(title: String, description: String, status: Status) -> Ticket {
+     pub fn new(title: String, description: String, status: Status) -> Result<Ticket, String> {
        if title.is_empty() {
-             panic!("Title cannot be empty");
+             return Err("Title cannot be empty".to_string());
        }

        // 以降も同様の改変

        if title.len() > 50 {
            return Err("Title cannot be longer than 50 bytes".to_string());
        }
        if description.is_empty() {
            return Err("Description cannot be empty".to_string());
        }
        if description.len() > 500 {
            return Err("Description cannot be longer than 500 bytes".to_string());
        }

        Ok(Ticket {
            title,
            description,
            status,
        })
    }
}

パニックを使わずに Result 型を使うことによって エラーを値として扱えるようになります 。この Result 型は、「Rustは難しいから扱いたくないけど、Result型だけは〇〇にも実装されてくれないかな...」と他言語でももっぱら人気だったり有名だったりする機能です!

Result 型もそれ自体の定義はとてもシンプルです!

Rust
enum Result<T, E> {
    Ok(T),
    Err(E),
}

別にオレオレ Result 型もいくらでも使用して良く、筆者も面倒な時はよく EString にして運用したりします、というか演習問題もそうなっています。( E にはトレイト境界をつけることが多いですがそれはまた別な機会とかに...)

何が言いたいかというと、 Option 型と同じで慣れてしまうと何も怖くなく分かりやすいということです。複雑な例外のためのフローや構文を覚える必要はありません!

Option 型のところで言い忘れましたが、 Option 型も Result 型も、 関数のシグネチャに入ってくる という良さがあります。「この関数は None を返してくるかもしれない」、「この関数は Result を返してくるかもしれない」ということが、シグネチャを見るだけで分かるようになります。(一昔前の)他言語だと NULL や例外に怯えドキュメントやソースコードを良く読まなければなりませんが、Rustでは型システムがどうなっているかを読むだけで、つまり眼の前のソースコードとIDE(VSCode)の補完だけで解決できてしまうことがしばしばあります。

[05_ticket_v2/07_unwrap] とりあえず .unwrap()

問題はこちらです。

lib.rs
// TODO: `easy_ticket` should panic when the title is invalid.
//   When the description is invalid, instead, it should use a default description:
//   "Description not provided".
fn easy_ticket(title: String, description: String, status: Status) -> Ticket {
    todo!()
}

#[derive(Debug, PartialEq, Clone)]
struct Ticket {
    title: String,
    description: String,
    status: Status,
}

// 以降テスト以外は前回の回答と同じ
全体
lib.rs
// TODO: `easy_ticket` should panic when the title is invalid.
//   When the description is invalid, instead, it should use a default description:
//   "Description not provided".
fn easy_ticket(title: String, description: String, status: Status) -> Ticket {
    todo!()
}

#[derive(Debug, PartialEq, Clone)]
struct Ticket {
    title: String,
    description: String,
    status: Status,
}

#[derive(Debug, PartialEq, Clone)]
enum Status {
    ToDo,
    InProgress { assigned_to: String },
    Done,
}

impl Ticket {
    pub fn new(title: String, description: String, status: Status) -> Result<Ticket, String> {
        if title.is_empty() {
            return Err("Title cannot be empty".to_string());
        }
        if title.len() > 50 {
            return Err("Title cannot be longer than 50 bytes".to_string());
        }
        if description.is_empty() {
            return Err("Description cannot be empty".to_string());
        }
        if description.len() > 500 {
            return Err("Description cannot be longer than 500 bytes".to_string());
        }

        Ok(Ticket {
            title,
            description,
            status,
        })
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use common::{overly_long_description, overly_long_title, valid_description, valid_title};

    #[test]
    #[should_panic(expected = "Title cannot be empty")]
    fn title_cannot_be_empty() {
        easy_ticket("".into(), valid_description(), Status::ToDo);
    }

    #[test]
    fn template_description_is_used_if_empty() {
        let ticket = easy_ticket(valid_title(), "".into(), Status::ToDo);
        assert_eq!(ticket.description, "Description not provided");
    }

    #[test]
    #[should_panic(expected = "Title cannot be longer than 50 bytes")]
    fn title_cannot_be_longer_than_fifty_chars() {
        easy_ticket(overly_long_title(), valid_description(), Status::ToDo);
    }

    #[test]
    fn template_description_is_used_if_too_long() {
        let ticket = easy_ticket(valid_title(), overly_long_description(), Status::ToDo);
        assert_eq!(ticket.description, "Description not provided");
    }
}

Description周りのエラーの時はデフォルトとなる値をセットして、それ以外の時はパニックするようにしてほしいという問題です。

解説

Err(E) の時はパニックにしてしまう unwrap を利用します。一番上の main 関数などでは unwrap を使ってパニックしてしまうのが楽ですね。なんだかんだ Result 型で一番お世話になるメソッドです。

lib.rs
fn easy_ticket(title: String, description: String, status: Status) -> Ticket {
    let ticket_w = Ticket::new(title.clone(), description, status.clone());

    match ticket_w {
        Ok(ticket) => ticket,
        Err(s) if s.starts_with("Description") => {
            Ticket::new(title, "Description not provided".to_string(), status).unwrap()
        }
        // t => t.unwrap(), // 網羅性は良いけど可読性的には良くない
        Err(s) => panic!("{}", s), // 網羅性は見づらいが可読性は良い
    }
}

この回まで使っていなかった機能として、 match 式のパターンマッチの枝に if 式のようなものを付けていますが、これはマッチガードと呼ばれるものです。 match 式で書きたいのだけどパターンとしては隠れてしまう条件を書くのにちょうどよい機能になっています。

マッチガードを使い、 Description で始まっているエラーの時だけデフォルトのDescriptionになっているチケットを返すようにしています。

パニックに関してですが、いくつか書き方があります。コメントに書いた通りなのでお好みのものを選べば良さそうです。筆者は Err(s) 派かなぁ...場合にもよりそうです。

[05_ticket_v2/08_error_enums] エラーも列挙体で管理しよう

問題はこちらです。

lib.rs
// TODO: Use two variants, one for a title error and one for a description error.
//   Each variant should contain a string with the explanation of what went wrong exactly.
//   You'll have to update the implementation of `Ticket::new` as well.
enum TicketNewError {}

// TODO: `easy_ticket` should panic when the title is invalid, using the error message
//   stored inside the relevant variant of the `TicketNewError` enum.
//   When the description is invalid, instead, it should use a default description:
//   "Description not provided".
fn easy_ticket(title: String, description: String, status: Status) -> Ticket {
    todo!()
}

#[derive(Debug, PartialEq)]
struct Ticket {
    title: String,
    description: String,
    status: Status,
}

// 省略
テストを含めた全体
lib.rs
// TODO: Use two variants, one for a title error and one for a description error.
//   Each variant should contain a string with the explanation of what went wrong exactly.
//   You'll have to update the implementation of `Ticket::new` as well.
enum TicketNewError {}

// TODO: `easy_ticket` should panic when the title is invalid, using the error message
//   stored inside the relevant variant of the `TicketNewError` enum.
//   When the description is invalid, instead, it should use a default description:
//   "Description not provided".
fn easy_ticket(title: String, description: String, status: Status) -> Ticket {
    todo!()
}

#[derive(Debug, PartialEq)]
struct Ticket {
    title: String,
    description: String,
    status: Status,
}

#[derive(Debug, PartialEq, Clone)]
enum Status {
    ToDo,
    InProgress { assigned_to: String },
    Done,
}

impl Ticket {
    pub fn new(
        title: String,
        description: String,
        status: Status,
    ) -> Result<Ticket, TicketNewError> {
        if title.is_empty() {
            return Err("Title cannot be empty".to_string());
        }
        if title.len() > 50 {
            return Err("Title cannot be longer than 50 bytes".to_string());
        }
        if description.is_empty() {
            return Err("Description cannot be empty".to_string());
        }
        if description.len() > 500 {
            return Err("Description cannot be longer than 500 bytes".to_string());
        }

        Ok(Ticket {
            title,
            description,
            status,
        })
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use common::{overly_long_description, overly_long_title, valid_description, valid_title};

    #[test]
    #[should_panic(expected = "Title cannot be empty")]
    fn title_cannot_be_empty() {
        easy_ticket("".into(), valid_description(), Status::ToDo);
    }

    #[test]
    fn template_description_is_used_if_empty() {
        let ticket = easy_ticket(valid_title(), "".into(), Status::ToDo);
        assert_eq!(ticket.description, "Description not provided");
    }

    #[test]
    #[should_panic(expected = "Title cannot be longer than 50 bytes")]
    fn title_cannot_be_longer_than_fifty_chars() {
        easy_ticket(overly_long_title(), valid_description(), Status::ToDo);
    }

    #[test]
    fn template_description_is_used_if_too_long() {
        let ticket = easy_ticket(valid_title(), overly_long_description(), Status::ToDo);
        assert_eq!(ticket.description, "Description not provided");
    }
}

先程の問題でマッチガードを利用してスタイリッシュに match 式で分岐を書きましたが、「複数の値を取りうる...?そもそも列挙体で良いのでは...?」ということでそのリファクタをし、エラーの内容も列挙体で表そうという問題です!

解説

エラーは2種類なので、2種類のバリアントを持つ列挙体 TicketNewError を定義します。 match 式の枝の方は、パターンマッチはネストして表現できることを利用し、 DescriptionError の時はデフォルトのDescriptionを返すようにしています。

lib.rs (前半)
#[derive(Debug)]
enum TicketNewError {
    TitleError(String),
    DescriptionError(String),
}

fn easy_ticket(title: String, description: String, status: Status) -> Ticket {
    let ticket_w = Ticket::new(title.clone(), description, status.clone());

    use TicketNewError::*;

    match ticket_w {
        Ok(ticket) => ticket,
        Err(TitleError(s)) => panic!("{}", s),
        Err(DescriptionError(_)) => {
            Ticket::new(title, "Description not provided".to_string(), status).unwrap()
        }
    }
}

後半の方も列挙体を使う方法に直す必要がありますね。

lib.rs (後半)
impl Ticket {
    pub fn new(
        title: String,
        description: String,
        status: Status,
    ) -> Result<Ticket, TicketNewError> {
        if title.is_empty() {
-             return Err("Title cannot be empty".to_string());
+             return Err(TicketNewError::TitleError(
+                 "Title cannot be empty".to_string(),
+             ));
        }

        // 以降のバリデーションにも同様の改変
        // ...省略...

        Ok(Ticket {
            title,
            description,
            status,
        })
    }
}

Result 型には慣れてきたでしょうか...?ここまで紹介してきた列挙型というシンプルなシステムを使っているだけなのに、大分複雑な見た目になってきました...逆に言うと、複雑に見えるRustの機能は案外こんな感じでシンプルということです。 Result 周りはもう少し複雑な感じになっていきますが、100 Exercisesは丁寧にフォローアップしていってくれるようです。

では次の問題に行きましょう!

次の記事: 【16】 Errorトレイトと外部クレート ~依存はCargo.tomlに全部お任せ!~

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