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

Rust 100 Ex 🏃【13/37】 トレイトまとめ・列挙型・match式 ~最強のトレイトの次は、最強の列挙型~

Last updated at Posted at 2024-07-17

前の記事

全記事一覧

100 Exercise To Learn Rust 演習第13回になります!今回でRustのキー要素であるトレイトの話が終わり、お次はもう一つの強烈な機能である列挙型の話に入っていきます。

今回の関連ページ

[04_traits/14_outro] トレイト付与復習

問題指示はこちらになります。

  • TODO: SaturatingU16 型を宣言してね!
    • u16 型の値を内包しています
    • u16, u8, &u16, &u8から変換することが可能です
    • SaturatingU16, u16, &u16, &SaturatingU16 との加算ができます
    • 加算の上限は u16 を超えないようにしてください。(飽和加算)
    • その他の SaturatingU16u16 型の変数と比較可能です
    • デバッグ出力を可能にしてください
  • テストは tests フォルダに有るよ!公開範囲( pub とか)に注意してね

解説

ここまでの復習ですね!実装あるのみです!

lib.rs
#[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章です!問題はこちらです。

lib.rs
// 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 フィールドでは ToDoInProgressDone の3つの値しか許したくないという要件がありました。

列挙型 (あるいは列挙体) Enum は、まさにこのような限定された状態しか取りたくない時に使用する機能です!

テストを含めた全体
lib.rs
// 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);
    }
}

解説

lib.rs
#[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
    }
}

以下に示す列挙型を導入することで、バリデーションを削除することが可能になりました!やったね!

Rust
enum Status {
    ToDo,
    InProgress,
    Done,
}

String よりも列挙体の方がプログラムを書く際にもミスしにくいのは明らかでしょう(タイポとか気にしなくて良くなるなど)。今後は積極的に使っていきたい...どころか5章の主役となります。

というのも、Rustの列挙体にはたくさんの強力な機能があるからです...とりあえず、次の問題に進みましょう。

[05_ticket_v2/02_match] match式

問題はこちらです。

lib.rs
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::CircleShape::Square という値として入ってきます。イメージとしては bool 型変数なら truefalse が値として取りうる、というのが、列挙型だと定義した数分ある感じでしょうか...?まぁ他言語と似たようなものだと思います。

どうやら列挙体 Shape のバリアントによって、 n_sides (頂点数)メソッドが返す値を変えたいようです。

テストを含めた全体
lib.rs
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 式同様こちらも式のため、最後に評価してほしい値をぽんと置いている感じになっています。

lib.rs
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なので、なんと整数型などにも以下のようにして応用可能です!

Rust
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に慣れれば慣れるほどその深みにハマっていくこと請け合いです!

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

次の記事: 【14】 フィールド付き列挙型とOption型 ~チョクワガタ~

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