前の記事
- 【0】 準備 ← 初回
- ...
- 【14】 フィールド付き列挙型とOption型 ~チョクワガタ~ ← 前回
- 【15】 Result型 ~Rust流エラーハンドリング術~ ← 今回
全記事一覧
- 【0】 準備
- 【1】 構文・整数・変数
- 【2】 if・パニック・演習
- 【3】 可変・ループ・オーバーフロー
- 【4】 キャスト・構造体 (たまにUFCS)
- 【5】 バリデーション・モジュールの公開範囲 ~ → カプセル化!~
- 【6】 カプセル化の続きと所有権とセッター ~そして不変参照と可変参照!~
- 【7】 スタック・ヒープと参照のサイズ ~メモリの話~
- 【8】 デストラクタ(変数の終わり)・トレイト ~終わりと始まり~
- 【9】 Orphan rule (孤児ルール)・演算子オーバーロード・derive ~Empowerment 💪 ~
- 【10】 トレイト境界・文字列・Derefトレイト ~トレイトのアレコレ~
- 【11】 Sized トレイト・From トレイト・関連型 ~おもしろトレイトと関連型~
- 【12】 Clone・Copy・Dropトレイト ~覚えるべき主要トレイトたち~
- 【13】 トレイトまとめ・列挙型・match式 ~最強のトレイトの次は、最強の列挙型~
- 【14】 フィールド付き列挙型とOption型 ~チョクワガタ~
- 【15】 Result型 ~Rust流エラーハンドリング術~
- 【16】 Errorトレイトと外部クレート ~依存はCargo.tomlに全部お任せ!~
- 【17】 thiserror・TryFrom ~トレイトもResultも自由自在!~
- 【18】 Errorのネスト・慣例的な書き方 ~Rustらしさの目醒め~
- 【19】 配列・動的配列 ~スタックが使われる配列と、ヒープに保存できる動的配列~
- 【20】 動的配列のリサイズ・イテレータ ~またまたトレイト登場!~
- 【21】 イテレータ・ライフタイム ~ライフタイム注釈ようやく登場!~
- 【22】 コンビネータ・RPIT ~ 「
Iterator
トレイトを実装してるやつ」~ - 【23】
impl Trait
・スライス ~配列の欠片~ - 【24】 可変スライス・下書き構造体 ~構造体で状態表現~
- 【25】 インデックス・可変インデックス ~インデックスもトレイト!~
- 【26】 HashMap・順序・BTreeMap ~Rustの辞書型~
- 【27】 スレッド・'staticライフタイム ~並列処理に見るRustの恩恵~
- 【28】 リーク・スコープ付きスレッド ~ライフタイムに技あり!~
- 【29】 チャネル・参照の内部可変性 ~Rustの虎の子、mpscと
Rc<RefCell<T>>
~ - 【30】 双方向通信・リファクタリング ~返信用封筒を入れよう!~
- 【31】 上限付きチャネル・PATCH機能 ~パンクしないように制御!~
- 【32】
Send
・排他的ロック(Mutex
)・非対称排他的ロック(RwLock
) ~真打Arc<Mutex<T>>
登場~ - 【33】 チャネルなしで実装・Syncの話 ~考察回です~
- 【34】
async fn
・非同期タスク生成 ~Rustの非同期入門~ - 【35】 非同期ランタイム・Futureトレイト ~非同期のお作法~
- 【36】 ブロッキング・非同期用の実装・キャンセル ~ラストスパート!~
- 【37】 Axumでクラサバ! ~最終回~
- 【おまけ1】 Rustで勘違いしていたこと3選 🏄🌴 【100 Exercises To Learn Rust 🦀 完走記事 🏃】
- 【おまけ2】 【🙇 懺悔 🙇】Qiitanグッズ欲しさに1日に33記事投稿した話またはQiita CLIとcargo scriptを布教する的な何か
100 Exercise To Learn Rust 演習第15回になります!
今回の関連ページ
[05_ticket_v2/06_fallibility] Rust用エラーハンドリング機構 Result型
問題はこちらです。
// 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,
}
}
}
テストを含めた全体
// 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回 の懸念事項が解消されます!
解説
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
型もそれ自体の定義はとてもシンプルです!
enum Result<T, E> {
Ok(T),
Err(E),
}
別にオレオレ Result
型もいくらでも使用して良く、筆者も面倒な時はよく E
を String
にして運用したりします、というか演習問題もそうなっています。( E
にはトレイト境界をつけることが多いですがそれはまた別な機会とかに...)
何が言いたいかというと、 Option
型と同じで慣れてしまうと何も怖くなく分かりやすいということです。複雑な例外のためのフローや構文を覚える必要はありません!
Option
型のところで言い忘れましたが、 Option
型も Result
型も、 関数のシグネチャに入ってくる という良さがあります。「この関数は None
を返してくるかもしれない」、「この関数は Result
を返してくるかもしれない」ということが、シグネチャを見るだけで分かるようになります。(一昔前の)他言語だと NULL
や例外に怯えドキュメントやソースコードを良く読まなければなりませんが、Rustでは型システムがどうなっているかを読むだけで、つまり眼の前のソースコードとIDE(VSCode)の補完だけで解決できてしまうことがしばしばあります。
[05_ticket_v2/07_unwrap] とりあえず .unwrap()
問題はこちらです。
// 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,
}
// 以降テスト以外は前回の回答と同じ
全体
// 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
型で一番お世話になるメソッドです。
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] エラーも列挙体で管理しよう
問題はこちらです。
// 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,
}
// 省略
テストを含めた全体
// 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を返すようにしています。
#[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()
}
}
}
後半の方も列挙体を使う方法に直す必要がありますね。
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は丁寧にフォローアップしていってくれるようです。
では次の問題に行きましょう!