前の記事
- 【0】 準備 ← 初回
- ...
- 【12】 Clone・Copy・Dropトレイト ~覚えるべき主要トレイトたち~ ← 前回
- 【13】 トレイトまとめ・列挙型・match式 ~ 最強のトレイトの次は、最強の列挙型 ~ ← 今回
全記事一覧
- 【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 演習第13回になります!今回でRustのキー要素であるトレイトの話が終わり、お次はもう一つの強烈な機能である列挙型の話に入っていきます。
今回の関連ページ
[04_traits/14_outro] トレイト付与復習
問題指示はこちらになります。
- TODO:
SaturatingU16
型を宣言してね!-
u16
型の値を内包しています -
u16
,u8
,&u16
,&u8
から変換することが可能です -
SaturatingU16
,u16
,&u16
,&SaturatingU16
との加算ができます - 加算の上限は
u16
を超えないようにしてください。(飽和加算) - その他の
SaturatingU16
かu16
型の変数と比較可能です - デバッグ出力を可能にしてください
-
- テストは
tests
フォルダに有るよ!公開範囲(pub
とか)に注意してね
解説
ここまでの復習ですね!実装あるのみです!
#[derive(Debug, Clone, Copy)]
pub struct SaturatingU16(u16);
macro_rules! impl_from {
($t:ty) => {
impl From<$t> for SaturatingU16 {
fn from(val: $t) -> Self {
Self(val.clone() as u16)
}
}
};
}
impl_from!(u16);
impl_from!(u8);
impl_from!(&u16);
impl_from!(&u8);
use std::ops::Add;
macro_rules! impl_add {
($t:ty) => {
impl Add<$t> for SaturatingU16 {
type Output = Self;
fn add(self, rhs: $t) -> Self::Output {
self.0
.saturating_add(SaturatingU16::from(rhs.clone()).0)
.into()
}
}
};
}
impl_add!(SaturatingU16);
impl_add!(&SaturatingU16);
impl_add!(u16);
impl_add!(&u16);
macro_rules! impl_eq {
($t:ty) => {
impl PartialEq<$t> for SaturatingU16 {
fn eq(&self, other: &$t) -> bool {
self.0 == SaturatingU16::from(other.clone()).0
}
}
};
}
impl_eq!(SaturatingU16);
impl Eq for SaturatingU16 {}
impl_eq!(u16);
トレイトとマクロで実装処理をまとめられると、「処理の全体像」が見えている感じがして爽快感があります!ありませんか...?
[05_ticket_v2/01_enum] 列挙型
本問題から5章です!問題はこちらです。
// TODO: use `Status` as type for `Ticket::status`
// Adjust the signature and implementation of all other methods as necessary.
#[derive(Debug, PartialEq)]
// `derive`s are recursive: it can only derive `PartialEq` if all fields also implement `PartialEq`.
// Same holds for `Debug`. Do what you must with `Status` to make this work.
struct Ticket {
title: String,
description: String,
status: String,
}
enum Status {
// TODO: add the missing variants
}
impl Ticket {
pub fn new(title: String, description: String, status: String) -> 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");
}
if status != "To-Do" && status != "In Progress" && status != "Done" {
panic!("Only `To-Do`, `In Progress`, and `Done` statuses are allowed");
}
Ticket {
title,
description,
status,
}
}
pub fn title(&self) -> &String {
&self.title
}
pub fn description(&self) -> &String {
&self.description
}
pub fn status(&self) -> &String {
&self.status
}
}
本問題で、 第5回で後ほど改善したいと言っていた部分の一つが改善されます...! status
フィールドでは ToDo
、 InProgress
、 Done
の3つの値しか許したくないという要件がありました。
列挙型 (あるいは列挙体) Enum は、まさにこのような限定された状態しか取りたくない時に使用する機能です!
テストを含めた全体
// TODO: use `Status` as type for `Ticket::status`
// Adjust the signature and implementation of all other methods as necessary.
#[derive(Debug, PartialEq)]
// `derive`s are recursive: it can only derive `PartialEq` if all fields also implement `PartialEq`.
// Same holds for `Debug`. Do what you must with `Status` to make this work.
struct Ticket {
title: String,
description: String,
status: String,
}
enum Status {
// TODO: add the missing variants
}
impl Ticket {
pub fn new(title: String, description: String, status: String) -> 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");
}
if status != "To-Do" && status != "In Progress" && status != "Done" {
panic!("Only `To-Do`, `In Progress`, and `Done` statuses are allowed");
}
Ticket {
title,
description,
status,
}
}
pub fn title(&self) -> &String {
&self.title
}
pub fn description(&self) -> &String {
&self.description
}
pub fn status(&self) -> &String {
&self.status
}
}
#[cfg(test)]
mod tests {
use super::*;
use common::{valid_description, valid_title};
#[test]
fn test_partial_eq() {
let title = valid_title();
let description = valid_description();
let ticket1 = Ticket {
title: title.clone(),
description: description.clone(),
status: Status::ToDo,
};
let ticket2 = Ticket {
title: title.clone(),
description: description.clone(),
status: Status::ToDo,
};
assert_eq!(ticket1, ticket2);
}
#[test]
fn test_description_not_matching() {
let title = valid_title();
let status = Status::ToDo;
let ticket1 = Ticket {
title: title.clone(),
description: "description".to_string(),
status,
};
let ticket2 = Ticket {
title: title.clone(),
description: "description2".to_string(),
status,
};
assert_ne!(ticket1, ticket2);
}
#[test]
fn test_title_not_matching() {
let description = valid_description();
let status = Status::InProgress;
let ticket1 = Ticket {
title: "title".to_string(),
description: description.clone(),
status,
};
let ticket2 = Ticket {
title: "title2".to_string(),
description: description.clone(),
status,
};
assert_ne!(ticket1, ticket2);
}
#[test]
fn test_status_not_matching() {
let title = valid_title();
let description = valid_description();
let ticket1 = Ticket {
title: title.clone(),
description: description.clone(),
status: Status::InProgress,
};
let ticket2 = Ticket {
title: title.clone(),
description: description.clone(),
status: Status::Done,
};
assert_ne!(ticket1, ticket2);
}
}
解説
#[derive(Debug, PartialEq)]
struct Ticket {
title: String,
description: String,
- status: String,
+ status: Status,
}
+ #[derive(Debug, PartialEq, Clone, Copy)]
enum Status {
+ ToDo,
+ InProgress,
+ Done,
}
impl Ticket {
- pub fn new(title: String, description: String, status: String) -> Ticket {
+ pub fn new(title: String, description: String, status: Status) -> Ticket {
// 他のバリデーション省略
- if status != "To-Do" && status != "In Progress" && status != "Done" {
- panic!("Only `To-Do`, `In Progress`, and `Done` statuses are allowed");
- }
Ticket {
title,
description,
status,
}
}
// 他のゲッター省略
- pub fn status(&self) -> &String {
+ pub fn status(&self) -> &Status {
&self.status
}
}
以下に示す列挙型を導入することで、バリデーションを削除することが可能になりました!やったね!
enum Status {
ToDo,
InProgress,
Done,
}
String
よりも列挙体の方がプログラムを書く際にもミスしにくいのは明らかでしょう(タイポとか気にしなくて良くなるなど)。今後は積極的に使っていきたい...どころか5章の主役となります。
というのも、Rustの列挙体にはたくさんの強力な機能があるからです...とりあえず、次の問題に進みましょう。
[05_ticket_v2/02_match] match式
問題はこちらです。
enum Shape {
Circle,
Square,
Rectangle,
Triangle,
Pentagon,
}
impl Shape {
// TODO: Implement the `n_sides` method using a `match`.
pub fn n_sides(&self) -> u8 {
todo!()
}
}
Shape
型変数 self
の値はここでは Shape::Circle
や Shape::Square
という値として入ってきます。イメージとしては bool
型変数なら true
や false
が値として取りうる、というのが、列挙型だと定義した数分ある感じでしょうか...?まぁ他言語と似たようなものだと思います。
どうやら列挙体 Shape
のバリアントによって、 n_sides
(頂点数)メソッドが返す値を変えたいようです。
テストを含めた全体
enum Shape {
Circle,
Square,
Rectangle,
Triangle,
Pentagon,
}
impl Shape {
// TODO: Implement the `n_sides` method using a `match`.
pub fn n_sides(&self) -> u8 {
todo!()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_circle() {
assert_eq!(Shape::Circle.n_sides(), 0);
}
#[test]
fn test_square() {
assert_eq!(Shape::Square.n_sides(), 4);
}
#[test]
fn test_rectangle() {
assert_eq!(Shape::Rectangle.n_sides(), 4);
}
#[test]
fn test_triangle() {
assert_eq!(Shape::Triangle.n_sides(), 3);
}
#[test]
fn test_pentagon() {
assert_eq!(Shape::Pentagon.n_sides(), 5);
}
}
解説
「パターンマッチ」に従って条件分岐が可能な match
"式"を利用して実装します。 if
式同様こちらも式のため、最後に評価してほしい値をぽんと置いている感じになっています。
impl Shape {
pub fn n_sides(&self) -> u8 {
// ここでuse文を使うことでちょっと楽できます
use Shape::*;
match self {
Circle => 0,
Square => 4,
Rectangle => 4,
Triangle => 3,
Pentagon => 5,
}
}
}
この時点で覚えておきたいことは、「 match
式は(というよりはパターンマッチは)網羅的」であることです。ためしに Pentagon
を抜いてみたりすると怒られます。逆に言えばパターンが網羅されていればおkなので、なんと整数型などにも以下のようにして応用可能です!
use rand::prelude::*;
fn main() {
let mut rng = thread_rng();
let v: u8 = rng.gen();
match v {
0 => println!("ゼロ!すごい!"),
1..=127 => println!("前半"),
128..=255 => println!("後半"),
}
}
また、第9回で少しだけ触れてはいましたが、「パターンマッチ」という機能も初登場しています。Rustのパターンマッチはかなり強力な機能で、Rustに慣れれば慣れるほどその深みにハマっていくこと請け合いです!
では次の問題に行きましょう!