前の記事
- 【0】 準備 ← 初回
- ...
- 【16】 Errorトレイトと外部クレート ~依存はCargo.tomlに全部お任せ!~ ← 前回
- 【17】 thiserror・TryFrom ~トレイトもResultも自由自在!~ ← 今回
全記事一覧
- 【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 演習第17回になります!今回から投稿マラソンの関係で問題数を3つから2つに減らします、ご了承ください。
今回の関連ページ
[05_ticket_v2/12_thiserror] 便利クレート thiserror
問題はこちらです。
// TODO: Implement the `Error` trait for `TicketNewError` using `thiserror`.
// We've changed the enum variants to be more specific, thus removing the need for storing
// a `String` field into each variant.
// You'll also have to add `thiserror` as a dependency in the `Cargo.toml` file.
enum TicketNewError {
TitleCannotBeEmpty,
TitleTooLong,
DescriptionCannotBeEmpty,
DescriptionTooLong,
}
#[derive(Debug, PartialEq, Clone)]
struct Ticket {
title: String,
description: String,
status: Status,
}
#[derive(Debug, PartialEq, Clone)]
enum Status {
ToDo,
InProgress { assigned_to: String },
Done,
}
// ...省略...
テストを含めた全体
// TODO: Implement the `Error` trait for `TicketNewError` using `thiserror`.
// We've changed the enum variants to be more specific, thus removing the need for storing
// a `String` field into each variant.
// You'll also have to add `thiserror` as a dependency in the `Cargo.toml` file.
enum TicketNewError {
TitleCannotBeEmpty,
TitleTooLong,
DescriptionCannotBeEmpty,
DescriptionTooLong,
}
#[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, TicketNewError> {
if title.is_empty() {
return Err(TicketNewError::TitleCannotBeEmpty);
}
if title.len() > 50 {
return Err(TicketNewError::TitleTooLong);
}
if description.is_empty() {
return Err(TicketNewError::DescriptionCannotBeEmpty);
}
if description.len() > 500 {
return Err(TicketNewError::DescriptionTooLong);
}
Ok(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 err = Ticket::new("".into(), valid_description(), Status::ToDo).unwrap_err();
assert_eq!(err.to_string(), "Title cannot be empty");
}
#[test]
fn description_cannot_be_empty() {
let err = Ticket::new(valid_title(), "".into(), Status::ToDo).unwrap_err();
assert_eq!(err.to_string(), "Description cannot be empty");
}
#[test]
fn title_cannot_be_longer_than_fifty_chars() {
let err = Ticket::new(overly_long_title(), valid_description(), Status::ToDo).unwrap_err();
assert_eq!(err.to_string(), "Title cannot be longer than 50 bytes");
}
#[test]
fn description_cannot_be_too_long() {
let err = Ticket::new(valid_title(), overly_long_description(), Status::ToDo).unwrap_err();
assert_eq!(
err.to_string(),
"Description cannot be longer than 500 bytes"
);
}
}
前回複数のエクササイズに跨いで準備してきましたが、今回ようやく thiserror
クレートを導入します!
解説
何はともあれまずは cargo add
コマンドです。
cargo add thiserror
これで dependencies
に thiserror
が加わります。
[package]
name = "thiserror_"
version = "0.1.0"
edition = "2021"
[dependencies]
+ thiserror = "1.0.61"
[dev-dependencies]
common = { path = "../../../helpers/common" }
(上記で dev-dependencies
は、テストのみに必要な依存を書くところになります。)
thiserror
クレートが提供するderiveマクロ Error
が使用できるようになるので、これを利用して Error
トレイトを実装します。
#[derive(thiserror::Error, Debug)]
enum TicketNewError {
#[error("Title cannot be empty")]
TitleCannotBeEmpty,
#[error("Title cannot be longer than 50 bytes")]
TitleTooLong,
#[error("Description cannot be empty")]
DescriptionCannotBeEmpty,
#[error("Description cannot be longer than 500 bytes")]
DescriptionTooLong,
}
これだけで エラー文付きで Error
トレイトを実装 してくれます! Clone
トレイトの時にもあったメリットですが、deriveマクロを使うことで、「楽にトレイトを実装できる」他、「宣言的に、漏れなく実装できる」というメリットがあります!
裏で何をしてくれているかを調べるためにここまでの演習がありましたが、意味がわかった今はマクロを使っていきたいです。
[05_ticket_v2/13_try_from] TryFrom トレイト
問題はこちらです。
// TODO: Implement `TryFrom<String>` and `TryFrom<&str>` for `Status`.
// The parsing should be case-insensitive.
#[derive(Debug, PartialEq, Clone)]
enum Status {
ToDo,
InProgress,
Done,
}
#[cfg(test)]
mod tests {
use super::*;
use std::convert::TryFrom;
#[test]
fn test_try_from_string() {
let status = Status::try_from("ToDO".to_string()).unwrap();
assert_eq!(status, Status::ToDo);
let status = Status::try_from("inproGress".to_string()).unwrap();
assert_eq!(status, Status::InProgress);
let status = Status::try_from("Done".to_string()).unwrap();
assert_eq!(status, Status::Done);
}
#[test]
fn test_try_from_str() {
let status = Status::try_from("todo").unwrap();
assert_eq!(status, Status::ToDo);
let status = Status::try_from("inprogress").unwrap();
assert_eq!(status, Status::InProgress);
let status = Status::try_from("done").unwrap();
assert_eq!(status, Status::Done);
}
}
解説
TryFrom/TryInto
は、キャスト用トレイト From/Into
の失敗する可能性がある版です!関連型としてエラーの時に返す型を指定し、対応する Result
型を返す処理を実装します。
use Status::*;
fn str2status(value: &str) -> Result<Status, String> {
match value.to_lowercase().as_str() {
"todo" => Ok(ToDo),
"inprogress" => Ok(InProgress),
"done" => Ok(Done),
s => Err(s.to_string()),
}
}
impl TryFrom<&str> for Status {
type Error = String;
fn try_from(value: &str) -> Result<Self, Self::Error> {
str2status(value)
}
}
impl TryFrom<String> for Status {
type Error = String;
fn try_from(value: String) -> Result<Self, Self::Error> {
str2status(&value)
}
}
String
から Status
への変換は今までのエクササイズでも似たような処理がしばしば登場していましたが、
- 列挙型の使用
-
TryFrom
による実装
によってかなり慣例的で読みやすい形となりました!たとえば String::from("ToDo").try_into()?
と書いてあれば、他のプログラマはこの部分を見ただけで「ここは文字列からの失敗する可能性のあるキャストなんだな」とわかります。
では次の問題に行きましょう!