1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

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

Rust 100 Ex 🏃【6/37】 カプセル化の続きと所有権とセッター ~そして不変参照と可変参照!~

Last updated at Posted at 2024-07-17

前の記事

全記事一覧

100 Exercise To Learn Rust 演習第6回になります!

今回の関連ページ

[03_ticket_v1/05_encapsulation] カプセル化

問題はこちらです。

lib.rs
pub mod ticket {
    pub struct Ticket {
        title: String,
        description: String,
        status: String,
    }

    impl Ticket {
        pub fn new(title: String, description: String, status: String) -> Ticket {
            // 省略
        }

        // TODO: Add three public methods to the `Ticket` struct:
        //  - `title` that returns the `title` field.
        //  - `description` that returns the `description` field.
        //  - `status` that returns the `status` field.
    }
}
テストを含めた全体
lib.rs
pub mod ticket {
    pub struct Ticket {
        title: String,
        description: String,
        status: String,
    }

    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,
            }
        }

        // TODO: Add three public methods to the `Ticket` struct:
        //  - `title` that returns the `title` field.
        //  - `description` that returns the `description` field.
        //  - `status` that returns the `status` field.
    }
}

#[cfg(test)]
mod tests {
    use super::ticket::Ticket;

    #[test]
    fn description() {
        let ticket = Ticket::new("A title".into(), "A description".into(), "To-Do".into());
        assert_eq!(ticket.description(), "A description");
    }

    #[test]
    fn title() {
        let ticket = Ticket::new("A title".into(), "A description".into(), "To-Do".into());
        assert_eq!(ticket.title(), "A title");
    }

    #[test]
    fn status() {
        let ticket = Ticket::new("A title".into(), "A description".into(), "To-Do".into());
        assert_eq!(ticket.status(), "To-Do");
    }
}

前回保留になっていたゲッターを設ける問題ですね...!

解説

lib.rs
impl Ticket {
    pub fn new(title: String, description: String, status: String) -> Ticket {
        // 省略
    }

    pub fn title(self) -> String {
        self.title
    }

    pub fn description(self) -> String {
        self.description
    }

    pub fn status(self) -> String {
        self.status
    }
}

手動実装の必要がありますが(deriveマクロとか探せばありそう...?まぁ要らないことが多い...)、同じモジュール内ならフィールドにアクセスできるわけですから、フィールドにアクセスするメソッドだけを公開してしまえばよいのです。

外部からフィールドに値をセットすることはできないので、これで目的は達成されます!

...まぁRust名物の 所有権的な問題が残っている のですが...それは次のエクササイズになります!

[03_ticket_v1/06_ownership] 所有権

問題はこちらです。

lib.rs
// TODO: based on what we just learned about ownership, it sounds like immutable references
//   are a good fit for our accessor methods.
//   Change the existing implementation of `Ticket`'s accessor methods take a reference
//   to `self` as an argument, rather than taking ownership of it.

pub struct Ticket {
    title: String,
    description: String,
    status: String,
}

impl Ticket {
    pub fn new(title: String, description: String, status: String) -> Ticket {
        // 省略
    }

    pub fn title(self) -> String {
        self.title
    }

    pub fn description(self) -> String {
        self.description
    }

    pub fn status(self) -> String {
        self.status
    }
}
テストを含めた全体
lib.rs
// TODO: based on what we just learned about ownership, it sounds like immutable references
//   are a good fit for our accessor methods.
//   Change the existing implementation of `Ticket`'s accessor methods take a reference
//   to `self` as an argument, rather than taking ownership of it.

pub struct Ticket {
    title: String,
    description: String,
    status: String,
}

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::Ticket;

    #[test]
    fn works() {
        let ticket = Ticket::new("A title".into(), "A description".into(), "To-Do".into());
        // If you change the signatures as requested, this should compile:
        // we can call these methods one after the other because they borrow `self`
        // rather than taking ownership of it.
        assert_eq!(ticket.title(), "A title");
        assert_eq!(ticket.description(), "A description");
        assert_eq!(ticket.status(), "To-Do");
    }
}

「所有権を考慮すると不変参照を使って書いたほうが良さそうなので書き直しましょう!」という問題です。つまり先の回答はRustを多少知っている人なら恣意的なものであったことがわかるわけです。

所有権についてめっちゃ解説したい気持ちがありますが、ここはBookの方をぜひ読んでほしいなと思うのであえてしません。

解説

不変参照でアクセスするように変えましょう!

lib.rs
impl Ticket {
    pub fn new(title: String, description: String, status: String) -> Ticket {
        // 省略
    }

-    pub fn title(self) -> String {
+    pub fn title(&self) -> &String {
-        self.title
+        &self.title
    }

    // 以降も同様の改変

    pub fn description(&self) -> &String {
        &self.description
    }

    pub fn status(&self) -> &String {
        &self.status
    }
}

ちなみに ticket.title() なども本来なら (&ticket).title() などに直さないといけないように感じますが、不変参照や可変参照で構造体にアクセスするのはRustではかなり頻出なパターンなので、忖度してもらえるため書き直す必要はなかったりします。便利

所有権及び不変参照への所感。よく「所有権が難しい」と聞きますが、Rustに慣れてくると他言語の仕様の方が(難しいというよりは)不安になってきます。え?だって 所有権がなかったら確保したリソースがどんなタイミングで解放されてNULLになるかコード「全部」を読まないとわからない じゃないですか...!?それぐらいなら所有権と参照を基軸にして確実にアクセスできることがコンパイル時点で確定してくれている方が数億倍わかりやすいです。Unityの並行処理みたいなソースコードでN敗した末に所有権をとてもありがたい存在だと思うようになったのでした1

小規模コードでスタックしか使われないような数値計算しかしないなら、参照という概念は省いて全部クローンされるような言語の方が読みやすい気もしますが、所有権や不変参照は慣れてしまえば全然読むのが苦じゃなくなるので、結局好みに終始しそうです。

[03_ticket_v1/07_setters] 可変参照とセッター

問題はこちらです。

lib.rs
// TODO: Add &mut-setters to the `Ticket` struct for each of its fields.
//   Make sure to enforce the same validation rules you have in `Ticket::new`!
//   Even better, extract that logic and reuse it in both places. You can use
//   private functions or private static methods for that.

pub struct Ticket {
    title: String,
    description: String,
    status: String,
}

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
    }
}
テストを含めた全体
lib.rs
// TODO: Add &mut-setters to the `Ticket` struct for each of its fields.
//   Make sure to enforce the same validation rules you have in `Ticket::new`!
//   Even better, extract that logic and reuse it in both places. You can use
//   private functions or private static methods for that.

pub struct Ticket {
    title: String,
    description: String,
    status: String,
}

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::Ticket;
    use common::{overly_long_description, overly_long_title, valid_description, valid_title};

    #[test]
    fn works() {
        let mut ticket = Ticket::new("A title".into(), "A description".into(), "To-Do".into());
        ticket.set_title("A new title".into());
        ticket.set_description("A new description".into());
        ticket.set_status("Done".into());

        assert_eq!(ticket.title(), "A new title");
        assert_eq!(ticket.description(), "A new description");
        assert_eq!(ticket.status(), "Done");
    }

    #[test]
    #[should_panic(expected = "Title cannot be empty")]
    fn title_cannot_be_empty() {
        Ticket::new(valid_title(), valid_description(), "To-Do".into()).set_title("".into());
    }

    #[test]
    #[should_panic(expected = "Description cannot be empty")]
    fn description_cannot_be_empty() {
        Ticket::new(valid_title(), valid_description(), "To-Do".into()).set_description("".into());
    }

    #[test]
    #[should_panic(expected = "Title cannot be longer than 50 bytes")]
    fn title_cannot_be_longer_than_fifty_chars() {
        Ticket::new(valid_title(), valid_description(), "To-Do".into())
            .set_title(overly_long_title())
    }

    #[test]
    #[should_panic(expected = "Description cannot be longer than 500 bytes")]
    fn description_cannot_be_longer_than_500_chars() {
        Ticket::new(valid_title(), valid_description(), "To-Do".into())
            .set_description(overly_long_description())
    }

    #[test]
    #[should_panic(expected = "Only `To-Do`, `In Progress`, and `Done` statuses are allowed")]
    fn status_must_be_valid() {
        Ticket::new(valid_title(), valid_description(), "To-Do".into()).set_status("Funny".into());
    }
}

今度は 可変参照 を利用し「セッター」を書こうという問題です。セッターでもバリデーションをするようにしてほしいとのことです。

解説

lib.rs
impl Ticket {
    // newやゲッターは省略

    pub fn set_title(&mut self, title: String) {
        if title.is_empty() {
            panic!("Title cannot be empty");
        }
        if title.len() > 50 {
            panic!("Title cannot be longer than 50 bytes");
        }

        self.title = title;
    }

    pub fn set_description(&mut self, description: String) {
        if description.is_empty() {
            panic!("Description cannot be empty");
        }
        if description.len() > 500 {
            panic!("Description cannot be longer than 500 bytes");
        }

        self.description = description;
    }

    pub fn set_status(&mut self, status: String) {
        if status != "To-Do" && status != "In Progress" && status != "Done" {
            panic!("Only `To-Do`, `In Progress`, and `Done` statuses are allowed");
        }

        self.status = status;
    }
}

バリデーション部分は別なプライベート関数に抜き出しても良かったのですが、記事の見栄えを優先して全部展開してみました。共通化したほうが安牌でしょう(どうせ Result 等で書き直すだろうから丁寧に書かなかったのもある)

例によって可変参照周りで面白い話が書いてあるので、今回はBookの方も是非読んでほしいです。例えば変数のシャドーイングの話が書いていたりします。

Rust
fn main() {
    let hoge = 10;
    println!("{}", hoge);
    let hoge = 20; // もう一回変数宣言できる! hoge2 とかにする必要はなし!
    println!("{}", hoge);

    let mut i = 0;
    let mut j = i + 1;
    for _ in 0..5 {
        i += 1;
        // シャドーイングはあくまでも改めて宣言しているだけなので、
        // スコープを跨ぐことはできない
        // (ので可変な変数とは全く別な意味)
        let j = i + 1;
    }
    println!("{}", j); // 1
}

ここで話し足りない参照の話は、次の拙著に任せたいと思います。読んでいただけると幸いです! :qiitan:

Rustにはシャローコピーがわからない - Qiita

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

次の記事: 【7】 スタック・ヒープと参照のサイズ ~メモリの話~

  1. あんまり他技術の悪口を書くのは読者の方をいたずらに不快にさせるだけなので良くないですが、所有権に関しては性質上他言語での失敗を引き合いに出したほうが説明しやすいというのがあり...まぁ何が言いたいかというとUnity好きな人ごめんなさい。

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

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?