More than 1 year has passed since last update.
Rust 100 Ex 🏃【15/37】 Result型 ~Rust流エラーハンドリング術~
前の記事
- 【0】 準備 ← 初回
- ...
- 【14】 フィールド付き列挙型とOption型 ~チョクワガタ~ ← 前回
- 【15】 Result型 ~Rust流エラーハンドリング術~ ← 今回
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,
}
}
}
ついに... 第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,
}
// 以降テスト以外は前回の回答と同じ
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,
}
// 省略
先程の問題でマッチガードを利用してスタイリッシュに 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は丁寧にフォローアップしていってくれるようです。
では次の問題に行きましょう!
Register as a new user and use Qiita more conveniently
- You get articles that match your needs
- You can efficiently read back useful information
- You can use dark theme
