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

Rust 100 Ex 🏃【24/37】 可変スライス・下書き構造体 ~構造体で状態表現~

Last updated at Posted at 2024-07-17

前の記事

全記事一覧

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

今回の関連ページ

[06_ticket_management/11_mutable_slices] 可変スライス

問題はこちらです。

lib.rs
// TODO: Define a function named `lowercase` that converts all characters in a string to lowercase,
//  modifying the input in place.
//  Does it need to take a `&mut String`? Does a `&mut str` work? Why or why not?

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

    #[test]
    fn empty() {
        let mut s = String::from("");
        lowercase(&mut s);
        assert_eq!(s, "");
    }

    #[test]
    fn one_char() {
        let mut s = String::from("A");
        lowercase(&mut s);
        assert_eq!(s, "a");
    }

    #[test]
    fn multiple_chars() {
        let mut s = String::from("Hello, World!");
        lowercase(&mut s);
        assert_eq!(s, "hello, world!");
    }

    #[test]
    fn mut_slice() {
        let mut s = "Hello, World!".to_string();
        lowercase(s.as_mut_str());
        assert_eq!(s, "hello, world!");
    }
}

コメントを要約するとこんな感じです。

  • TODO: 全文字を小文字にする lowercase 関数を定義してください
    • 入力自体を修飾します(つまり可変参照を受け取る)
    • &mut String&mut str どちらで受け取ると良さそうでしょうか?その理由は?

理由まで聞かれているので解説では理由も回答したいなと思います。

解説

前回はスライス &[T] が登場した回でした!不変参照については静的系([T;N], &'static str)・ヒープ系(Vec<T>, String)問わず受け取れる &[T]&str が良いとされてきましたが、では可変参照でもそうした方が良いのか?という問題で、 Book の方では「そうとは限らない」といった趣旨の説明がなされています。

本問題への回答は次のような感じです。 &mut str 、すなわち str 型を使用する方を選択しました。

lib.rs
fn lowercase(s: &mut str) {
// fn lowercase(s: &mut String) { // どっち?という問題でこちらは選ばなかった
    s.make_ascii_lowercase();
}

以下、理由です。(作り込んだので堅苦しいです)

  • &mut strmake_ascii_lowercase メソッドを持っているため、引数を &mut str として受け取ることでも本処理を実現できます。(&mut str を引数に取れる十分性の説明)
    • しかし、 &'static str 型として扱われる文字列リテラルは(少なくとも safe Rust では)可変を取れないため、 &mut String を受け取っておけばほとんどのケースでは困りません。
  • 本エクササイズではテストケースにて &mut str を返す as_mut_str メソッドが呼ばれているので、 &mut str を引数型として用いなければなりません。 (&mut str を引数型として受け取らなければならないという必要性の説明)

ユースケースを考えれば &mut String でも良いだろうけど、問題の答えとしては &mut str として受け取る他ないという説明でした。まぁ &mut String だと関数内で何されるかわからないから利用者目線だとできることが制限される &mut str の方が嬉しいという考え方もありますが、関数を作るのも使うのも自分なら &mut String で良いと思いますね。

[06_ticket_management/12_two_states] 下書き構造体(ステートで構造体を分ける)

問題はこちらです。

lib.rs
// TODO: Update `add_ticket`'s signature: it should take a `TicketDraft` as input
//  and return a `TicketId` as output.
//  Each ticket should have a unique id, generated by `TicketStore`.
//  Feel free to modify `TicketStore` fields, if needed.
//
// You also need to add a `get` method that takes as input a `TicketId`
// and returns an `Option<&Ticket>`.

use ticket_fields::{TicketDescription, TicketTitle};

#[derive(Clone)]
pub struct TicketStore {
    tickets: Vec<Ticket>,
}

#[derive(Clone, Copy, Debug, PartialEq)]
pub struct TicketId(u64);

#[derive(Clone, Debug, PartialEq)]
pub struct Ticket {
    pub id: TicketId,
    pub title: TicketTitle,
    pub description: TicketDescription,
    pub status: Status,
}

#[derive(Clone, Debug, PartialEq)]
pub struct TicketDraft {
    pub title: TicketTitle,
    pub description: TicketDescription,
}

#[derive(Clone, Debug, Copy, PartialEq)]
pub enum Status {
    ToDo,
    InProgress,
    Done,
}

impl TicketStore {
    pub fn new() -> Self {
        Self {
            tickets: Vec::new(),
        }
    }

    // ここに改変が必要
    pub fn add_ticket(&mut self, ticket: Ticket) {
        self.tickets.push(ticket);
    }
}

#[cfg(test)]
mod tests {
    use crate::{Status, TicketDraft, TicketStore};
    use ticket_fields::test_helpers::{ticket_description, ticket_title};

    #[test]
    fn works() {
        let mut store = TicketStore::new();

        let draft1 = TicketDraft {
            title: ticket_title(),
            description: ticket_description(),
        };
        let id1 = store.add_ticket(draft1.clone());
        let ticket1 = store.get(id1).unwrap();
        assert_eq!(draft1.title, ticket1.title);
        assert_eq!(draft1.description, ticket1.description);
        assert_eq!(ticket1.status, Status::ToDo);

        let draft2 = TicketDraft {
            title: ticket_title(),
            description: ticket_description(),
        };
        let id2 = store.add_ticket(draft2);
        let ticket2 = store.get(id2).unwrap();

        assert_ne!(id1, id2);
    }
}

チケットマネージャを作るテーマに戻ってきました。 Vec<Ticket> に新たなチケットを加える add_ticket メソッドがありましたが、今回から Ticket 構造体それ自体ではなくその下書きである TicketDraft 構造体を引数として取り、IDを返すように改変し、ついでにストアからIDでチケットをオプショナルに取得する get メソッドを定義せよ、という問題です。

解説

指示通り引数の型を Ticket から、 idstatus がオミットされている TicketDraft に変更し、最後にIDを返すように改変すればおkです!

lib.rs
impl TicketStore {
    // ...省略...

    pub fn add_ticket(&mut self, ticket_draft: TicketDraft) -> TicketId {
        let TicketDraft { title, description } = ticket_draft;

        let id = TicketId(self.tickets.len() as _);

        let ticket = Ticket {
            title,
            id,
            description,
            status: Status::ToDo,
        };

        self.tickets.push(ticket);

        id
    }

    fn get(&self, id: TicketId) -> Option<&Ticket> {
        self.tickets.iter().find(|t| t.id == id)
    }
}

id は、現在の動的配列のサイズを渡すことで作成することにしました。この方法なら衝突しません。TicketIdu64 を要求する一方で、 .len()usize 型を返すのでキャストしています。(キャスト先の型が推論できる時は as _ と書くだけで良くて便利)

status は作成時点では ToDo と決め打ちしています。

get メソッドでは、とりあえず今回は find メソッドを使うことで最初に id が一致したものを返すという風にしました。( O(N) かかるのをなんとかするのがまさに次回以降の話題です!)

今回は目新しい話題や技術的に難しい内容はありませんでしたね。それはそれとしてBookの内容は今回も面白いです。

id をNULLABLEにする代わりに、 TicketTicketDraft という形でステートで分けて管理することで不正なオブジェクト(生焼けオブジェクト)の生成を防ぐ、というのはシンプルですが中々興味深いです。最終的には省かれますが、まず Option 型にしなければ表現できない id フィールドが発見され、そこから「生焼けオブジェクトを防ぐために構造体を分けたほうが良い」という気付きが得られるという流れは、NULL安全であることで初めて気づきやすくなる部分でしょう。

「ステートで構造体を分けてしまおう」というのは、クラスが無いため構造体の定義に対してフッ軽で、 Option 型があるからこそたどり着ける、ということで、Rustならではの考え方なんじゃないかと個人的には思います!

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

次の記事: 【25】 インデックス・可変インデックス ~インデックスもトレイト!~

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