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

Rust 100 Ex 🏃【10/37】 トレイト境界・文字列・Derefトレイト ~トレイトのアレコレ~

Last updated at Posted at 2024-07-17

前の記事

全記事一覧

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

今回の関連ページ

[04_traits/05_trait_bounds] トレイト境界

問題はこちらです。

lib.rs
// TODO: Add the necessary trait bounds to `min` so that it compiles successfully.
//   Refer to the documentation of the `std::cmp` module for more information on the traits you might need.
//
// Note: there are different trait bounds that'll make the compiler happy, but they come with
// different _semantics_. We'll cover those differences later in the course when we talk about ordered
// collections (e.g. BTreeMap).

/// Return the minimum of two values.
pub fn min<T>(left: T, right: T) -> T {
    if left <= right {
        left
    } else {
        right
    }
}

コンパイルが通るようにしてほしい問題ですね。早速解いちゃいましょう!

解説

今回から ジェネリクス が新登場しています。C++でいうところのテンプレートに似たような機能で、C#とかには普通に存在するものですね。今回は関数について存在していますが、例えば動的配列 Vec<T> やオプショナル構造体 Option<T> 等の構造体、そしてトレイト等様々なところで目にすることになります。

難しい言い方をするとジェネリクスはある意味で、「型を受け取って型を返す写像」と説明されることがあります。この説明も筆者は好きですが、「型のワイルドカード」とかもう少し直感的な捉え方でも良いかなと思ったり。

話が逸れましたが、今回、このジェネリクス関数のコンパイルを通すには「トレイト境界」を設定する必要があります。T: PartialEq と書くことによって、 T に「ただし PartialOrd を実装している型に限る」という制限を設けています。これがトレイト境界1です。

lib.rs
- pub fn min<T>(left: T, right: T) -> T {
+ pub fn min<T: PartialOrd>(left: T, right: T) -> T {
    if left <= right {
        left
    } else {
        right
    }
}

TPartialOrd トレイトが実装されていることが確約されているので、内部で PartialOrd が実装された型でしか使えない機能( <= )を使用できるようになっています。

トレイト境界はジェネリクス以外のシーンでも、例えばトレイトの定義とかでも登場します(その場合、super trait/sub traitという関係で呼ばれるらしいです)。「トレイトで self 変数が使えるのは奇妙に見えるかも...」みたいなことを前回か前々回か言った気がするのですが、トレイト境界はそのモヤモヤを取っ払ってくれる考えかもしれません。ワイルドカードにしたい型に対して、何でもかんでも許すのではなく、ある程度型に求められる条件が絞られるので、トレイト境界が設けられている場合はどんな型が入ってくるかある程度推測できたりします。

[04_traits/06_str_slice] String&str

問題はこちらです。

lib.rs
// TODO: Re-implement `Ticket`'s accessor methods. This time return a `&str` rather than a `&String`.

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

#[cfg(test)]
mod tests {
    use super::*;
    use common::{valid_description, valid_title};
    use std::any::{Any, TypeId};

    #[test]
    fn test_type() {
        let ticket = Ticket::new(valid_title(), valid_description(), "To-Do".to_string());
        // Some dark magic to verify that you used the expected return types
        assert_eq!(TypeId::of::<str>(), ticket.title().type_id());
        assert_eq!(TypeId::of::<str>(), ticket.description().type_id());
        assert_eq!(TypeId::of::<str>(), ticket.status().type_id());
    }
}
全体
lib.rs
// TODO: Re-implement `Ticket`'s accessor methods. This time return a `&str` rather than a `&String`.

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::*;
    use common::{valid_description, valid_title};
    use std::any::{Any, TypeId};

    #[test]
    fn test_type() {
        let ticket = Ticket::new(valid_title(), valid_description(), "To-Do".to_string());
        // Some dark magic to verify that you used the expected return types
        assert_eq!(TypeId::of::<str>(), ticket.title().type_id());
        assert_eq!(TypeId::of::<str>(), ticket.description().type_id());
        assert_eq!(TypeId::of::<str>(), ticket.status().type_id());
    }
}

このままだとテストで落ちてしまいます。指示通り Stringstr に直してみましょう。

解説

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

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

    // 他も同様

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

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

テストでは std::any::TypeId 関数を使って str のタイプIDを取得し、そのタイプIDと ticket.title() などのゲッターが返す型がちゃんと str ( &str )になっているか確認しているみたいです。

今回から登場した、そしてRustにおいて至るところで見る &str は文字列の欠片、文字列スライスと呼ばれるもので、大体はイミュータブル(不変)で文字列の一部を参照するのに使います。深く考える必要はなくて、 &String を使うシーンがあれば、 &str で置き換えた方が何かと便利と思っておく程度で良いと思います。(個別に考えなければならないシチュエーションを全て洗うのは大変...)

ところで &String&str とは異なる型のはずなのに、なぜコンパイルが通るのでしょうか?というのは、次のエクササイズでBookにて解説されています。

[04_traits/07_deref] Derefトレイト

String の参照 &String&str として振る舞えるのは、 StringDeref<Target = str> が実装されているからでした!」というのを踏まえた問題になります。トレイトはなんでも可能にしてしまいますね...

lib.rs
// TODO: whenever `title` and `description` are returned via their accessor methods, they
//   should be normalized—i.e. leading and trailing whitespace should be removed.
//   There is a method in Rust's standard library that can help with this, but you won't
//   find it in the documentation for `String`.
//   Can you figure out where it is defined and how to use it?

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

impl Ticket {
    pub fn title(&self) -> &str {
        todo!()
    }

    pub fn description(&self) -> &str {
        todo!()
    }
}
テストを含めた全体
lib.rs
// TODO: whenever `title` and `description` are returned via their accessor methods, they
//   should be normalized—i.e. leading and trailing whitespace should be removed.
//   There is a method in Rust's standard library that can help with this, but you won't
//   find it in the documentation for `String`.
//   Can you figure out where it is defined and how to use it?

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

impl Ticket {
    pub fn title(&self) -> &str {
        todo!()
    }

    pub fn description(&self) -> &str {
        todo!()
    }
}

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

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

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

解説

要は「 String 型にはなくて &str 型にはあるメソッドをドキュメントから探そう!」という問題です。 trim というメソッドを使うと文字列前後の空白文字(改行とかも含む)を除いてくれるので、これを使います。

lib.rs
impl Ticket {
    pub fn title(&self) -> &str {
        self.title.trim()
    }

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

ちなみに「〇〇にはなくて〇〇の Deref トレイト先なら持ってる」で多分この先n回お世話になるのは Vec<T> にとっての &[T] (スライス) じゃないかなと思います。 Vec<T> にしかないメソッド、 &[T] 側にあるメソッド、両方存在するので、メソッド名だけ聞いてもドキュメントをみないとどちらのメソッドかすぐにはわからないものがしばしばあるかなと思います。(さらにちなみに配列を扱うのは第19回とだいぶ先だったりします)

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

次の記事: 【11】 Sized トレイト・From トレイト・関連型 ~おもしろトレイトと関連型~

  1. ところで、trait boundの和訳として「トレイト境界」とされているらしい?ですが、しばしば議論の的になっているようですね。閑話休題

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