🦀

Rustの勉強 10日目

2022/10/23に公開

The Rust Programming Language 日本語版 第10章

しばらく日が開いてしまったが、また続きを行う。ジェネリクスとトレイトとライフタイムについて。

10.1 ジェネリックなデータ型

  • 多くのジェネリクスを持つ言語のように、型引数にまず T を使う。(これは慣例)
  • ジェネリクスを持つ関数の場合は次のように関数名の直後に型引数を与えて宣言する
fn largest<T>(list: &[T]) -> T { ... }
  • 関数内の T に対する振る舞い次第で T が特定のトレイトを持っている必要が有ることを指定する必要がでてくる
    • 例えば比較であれば std::cmp::PartialOrd トレイトを持つ必要がある
    • トレイトに関しては次節で説明
  • 構造体の定義では構造体名のあとに型引数を与える
    • フィールドごとに異なる型を持たせたかったら、型引数を複数用意してあげる
struct Point<T, U> {
    x: T,
    y: U,
}
  • Enumの定義でも同様
enum Result<T, E> {
    Ok(T),
    Err(E),
}
  • メソッドの定義の場合は impl キーワードの直後に型引数を与えることに注意
    • これでimpl宣言内での Point<T>T が具体的な型ではなくジェネリックな型であることを伝える
impl<T> Point<T> {
    fn x(&self) -> &T {
        &self.x
    }
}
  • Rustでは多相な型をコンパイル時に単相化することで実行スピードを犠牲にしないようにしている

10.2 トレイト: 共通の振る舞いを定義する

  • トレイトは他の言語でインターフェースと呼ばれる機能に似ている
  • trait キーワードで宣言
    • メソッドシグネチャを羅列する
    • トレイトを実装する際には impl キーワードに for で指定する
trait Summary {
    fn summerize(&self) -> String;
}

impl Summary for NewsArticle {
    fn summerize(&self) -> String {
        format!("{}, by {} ({})", self.headline, self.author, self.location)
    }
}
  • トレイトを公開したい場合には他と同様に pub キーワードで行う
  • 外部のトレイトを外部の型に対して実装することは不可能(これができてしまうと例えば標準ライブラリの型の振る舞いを勝手に変えることができてしまう)
  • デフォルトのメソッド定義をしておくと、トレイトの実装を簡便かつ共通にできる
    • カスタム実装がある場合にはオーバーライドされる
trait Summary {
    fn summarize(&self) -> String {
        String::from("(Read more...)")
    }
}

impl Summary for NewsArticle {} // summarizeはデフォルト実装が採用される
  • トレイトを関数の引数として受け取れる
    • fn notify(item: &impl Summary)
    • これはトレイト境界の糖衣構文
  • トレイトをジェネリック型の制約として指定できる(トレイト境界)
    • fn notify<T: Summary>(item: &T)
  • 複数の引数がある場合にはトレイト境界をきちんと与えるほうがわかりやすい気がする
    • たとえば次の例だと(1)と(2)は同じだけど、item1item2が同じ型であることを強制させるためには(3)のようにしないといけない
fn notify(item1: &impl Summary, item2: &impl Summary) {...} // (1)

fn notify<T: Summary, U: Summary>(item1: &T, item2: &U) {...} // (2)

fn notify<T: Summary>(item1: &T, item2: &T) {...} // (3)
  • 複数のトレイトを実装していることを求める場合には + で指定する
    • fn notify(item: &(impl Summary + Display))
    • fn notify<T: Summary + Display>(item: &T)
  • しかし実装すべきトレイとが多いときは where でトレイト境界を後置したほうが読みやすくなると思う
fn some_function<T, U>(t: &T, u: &U) -> i32
    where T: Display + Clone,
          U: Clone + Debug
{
    ...
}
  • ブランケット実装とかいう特定のトレイトを満たす型に対して実装するトレイトが強い
    • 次のコードは Display トレイトを満たすすべての型 T に対して ToString トレイトを実装している
impl<T: Display> ToString for T { ... }

10.3 ライフタイムで参照を検証する

  • ライフタイムとは「参照が有効になるスコープ」
  • Rustコンパイラには借用チェッカーなる機構があり、すべての借用が有効かを自動で決定する
  • ライフタイム注釈の型引数は 'a のようにアポストロフィーを与える必要がある
  • 関数内でどのデータにどういった処理を行っているかをよく考えてライフタイム注釈を与える必要がある
    • 次の関数は y を受け取っても返すわけではないのでライフタイムに影響しない
fn foo<'a>(x: &'a str, y: &str) -> &'a str {
  x
}
  • 構造体定義にライフタイム注釈を入れると、構造体のライフタイムが特定の参照を持つフィールドと同じになることが表現できる。
    • 構造体に参照もたせるの、難しすぎない???
    • 当然 impl を書く際にも同じライフタイム注釈が必要になる
// ImportantExcerptのライフタイムはpartフィールドと同じになる
struct ImportantExcerpt<'a> {
    part: &'a str,
}

impl<'a> ImportantExcerpt<'a> {
    fn level(&self) -> i32 {
        3
    }
}

fn main() {
    let novel = String::from("Call me Ishmael. Some years ago...");
    let first_sentence = novel.split('.').next().expect("Could not find a '.'");
    let i = ImportantExcerpt {
        part: first_sentence,
    };
}
  • ライフタイム省略規則: コンパイラが勝手に適用してライフタイム注釈の省略を可能にしてくれるもの

    • 最初の規則は、参照である各引数は、独自のライフタイム引数を得るというものです。(fn foo<'a, 'b>(x: &'a i32, y: &'b i32)
    • 2番目の規則は、1つだけ入力ライフタイム引数があるなら、そのライフタイムが全ての出力ライフタイム引数に代入されるというものです。(fn foo<'a>(x: &'a i32) -> &'a i32
    • 3番目の規則は、複数の入力ライフタイム引数があるけれども、メソッドなのでそのうちの一つが &self&mut self だったら、 selfのライフタイムが全出力ライフタイム引数に代入されるというものです。
  • 'static というプログラムの全期間において生存する静的ライフタイムというものが存在する

Rustlings

options1.rs

与えられた時間が [0..24] のどれかだったら Some を返して、それ以外だったら None を返す。条件にある通りの数字を返すだけ。テストも Some を扱うように変更。

fn maybe_icecream(time_of_day: u16) -> Option<u16> {
    // We use the 24-hour system here, so 10PM is a value of 22
    // The Option output should gracefully handle cases where time_of_day > 24.
    if time_of_day < 22 {
        Some(5)
    } else if 22 <= time_of_day && time_of_day < 25 {
        Some(0)
    } else {
        None
    }
}

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

    #[test]
    fn check_icecream() {
        assert_eq!(maybe_icecream(9), Some(5));
        assert_eq!(maybe_icecream(10), Some(5));
        assert_eq!(maybe_icecream(23), Some(0));
        assert_eq!(maybe_icecream(22), Some(0));
        assert_eq!(maybe_icecream(25), None);
    }

    #[test]
    fn raw_value() {
        let icecreams = maybe_icecream(12);
        assert_eq!(icecreams, Some(5));
    }
}

options2.rs

if let の書き方を忘れてしまってたのでもう一度ドキュメント見直した。match のアーム1個だけと思って書けばまあまあ思い出しやすいなと思った。

pop で戻り値が Option<Option<T>> になってることを見逃していたので、テストの assert を変更した。

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

    #[test]
    fn simple_option() {
        let target = "rustlings";
        let optional_target = Some(target);

        // TODO: Make this an if let statement whose value is "Some" type
        if let Some(word) = optional_target {
            assert_eq!(word, target);
        }
    }

    #[test]
    fn layered_option() {
        let mut range = 10;
        let mut optional_integers: Vec<Option<i8>> = Vec::new();
        for i in 0..(range + 1) {
            optional_integers.push(Some(i));
        }

        // TODO: make this a while let statement - remember that vector.pop also adds another layer of Option<T>
        // You can stack `Option<T>`'s into while let and if let
        while let Some(integer) = optional_integers.pop() {
            assert_eq!(integer, Some(range));
            range -= 1;
        }
    }
}

options3.rs

match の最初のアームで p がムーブされちゃってるので、参照しか使わないようにするために Some(ref p) に書き換えた。

struct Point {
    x: i32,
    y: i32,
}

fn main() {
    let y: Option<Point> = Some(Point { x: 100, y: 200 });

    match y {
        Some(ref p) => println!("Co-ordinates are {},{} ", &p.x, &p.y),
        _ => println!("no match"),
    }
    y; // Fix without deleting this line.
}

traits1.rs

トレイトで定義されている関数が実装されていなかったので、impl で実装した。 Self という大文字始まりのものがタイポではなく、トレイトを実装する構造体を指すことを知った。

trait AppendBar {
    fn append_bar(self) -> Self;
}

impl AppendBar for String {
    fn append_bar(self) -> String {
        String::from(self + "Bar")
    }
}

fn main() {
    let s = String::from("Foo");
    let s = s.append_bar();
    println!("s: {}", s);
}

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

    #[test]
    fn is_foo_bar() {
        assert_eq!(String::from("Foo").append_bar(), String::from("FooBar"));
    }

    #[test]
    fn is_bar_bar() {
        assert_eq!(
            String::from("").append_bar().append_bar(),
            String::from("BarBar")
        );
    }
}

traits2.rs

テストを見て、どうやら "Bar" という文字列をベクタに追加してるようだったので、push するコードを書いた

trait AppendBar {
    fn append_bar(self) -> Self;
}

impl AppendBar for Vec<String> {
    fn append_bar(mut self) -> Vec<String> {
        self.push(String::from("Bar"));
        self
    }
}

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

    #[test]
    fn is_vec_pop_eq_bar() {
        let mut foo = vec![String::from("Foo")].append_bar();
        assert_eq!(foo.pop().unwrap(), String::from("Bar"));
        assert_eq!(foo.pop().unwrap(), String::from("Foo"));
    }
}

traits3.rs

一瞬 version_number を返すのかと思って実装に困っていたが、よくテストを見たら単純に "Some information" という String を返せばいいだけだったのでデフォルト実装を書くだけで終わった。

pub trait Licensed {
    fn licensing_info(&self) -> String {
        String::from("Some information")
    }
}

struct SomeSoftware {
    version_number: i32,
}

struct OtherSoftware {
    version_number: String,
}

impl Licensed for SomeSoftware {} // Don't edit this line
impl Licensed for OtherSoftware {} // Don't edit this line

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

    #[test]
    fn is_licensing_info_the_same() {
        let licensing_info = String::from("Some information");
        let some_software = SomeSoftware { version_number: 1 };
        let other_software = OtherSoftware {
            version_number: "v2.0.0".to_string(),
        };
        assert_eq!(some_software.licensing_info(), licensing_info);
        assert_eq!(other_software.licensing_info(), licensing_info);
    }
}

traits4.rs

最初はトレイト境界を指定せずに直接的に書いていた。つまり次のような感じ。

fn compare_license_types(software: impl Licensed, software_two: impl Licensed) -> bool {
    software.licensing_info() == software_two.licensing_info()
}

しかしトレイト境界を書いたほうが、仮引数の型がスッキリすると思って書き換えてみたところ、特に変わらなかった。

pub trait Licensed {
    fn licensing_info(&self) -> String {
        "some information".to_string()
    }
}

struct SomeSoftware {}

struct OtherSoftware {}

impl Licensed for SomeSoftware {}
impl Licensed for OtherSoftware {}

fn compare_license_types<T: Licensed, U: Licensed>(software: T, software_two: U) -> bool {
    software.licensing_info() == software_two.licensing_info()
}

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

    #[test]
    fn compare_license_information() {
        let some_software = SomeSoftware {};
        let other_software = OtherSoftware {};

        assert!(compare_license_types(some_software, other_software));
    }

    #[test]
    fn compare_license_information_backwards() {
        let some_software = SomeSoftware {};
        let other_software = OtherSoftware {};

        assert!(compare_license_types(other_software, some_software));
    }
}

traits5.rs

SomeTraitOtherTrait を両方実装している型を渡す必要があったので、トレイト境界の + を使った表現で実装した。

pub trait SomeTrait {
    fn some_function(&self) -> bool {
        true
    }
}

pub trait OtherTrait {
    fn other_function(&self) -> bool {
        true
    }
}

struct SomeStruct {}
struct OtherStruct {}

impl SomeTrait for SomeStruct {}
impl OtherTrait for SomeStruct {}
impl SomeTrait for OtherStruct {}
impl OtherTrait for OtherStruct {}

// YOU MAY ONLY CHANGE THE NEXT LINE
fn some_func<T: SomeTrait + OtherTrait>(item: T) -> bool {
    item.some_function() && item.other_function()
}

fn main() {
    some_func(SomeStruct {});
    some_func(OtherStruct {});
}

とりあえず途中だけど、ライフタイムの問題はまた次の日に解く。

Discussion