🦀

The Rust Programing Language 8日目

2023/11/07に公開

前回のあらすじ
panic!Resultについて知る
今までプログラムの実行事例外に対しては「そういうもの」と言う感じで理解したフリをしてきたが、関数が例外を投げてくるんではなくResult型を返してくると言うのは理解しやすくていいなと思った
Javaであれば例外のハンドリングを書き忘れたりする所も、ResultならErrの処理を忘れることはない
(matchを網羅できずコンパイル通らないので)
まあpanic!の中で何が行われてるとかは結局わかってないが、それはそれ

本日は悪名高いライフタイムなどが含まれる10章 ジェネリック型、トレイト、ライフタイム

本日の学び

ジェネリクス

事前知識としてのジェネリクス、正直なんとなくの概念はわかるがちゃんと使えていない
汎用的に使える型の代用みたいなイメージ

  • fn largest<T>(list: &[T]) -> T {のように定義する
    • 関数largestは型Tに関してジェネリックである
    • 本筋じゃないけど、char型って大小比較できるの知らなかった、pythonもできるのか?
      • 辞書順で後が大きいという判定になるぽい
    • 取りうる全てのTに対して動作する必要がある
  • 構造体もジェネリックにできる(表現合ってる?)
    struct Point<T> {
        x: T,
        y: T,
    }
    
    fn main() {
        let integer = Point { x: 5, y: 10 };
        let float = Point { x: 1.0, y: 4.0 };
    }
    
    • この例ではxとyのフィールドは同じ型になっている
      • 別の型を使用する場合は、複数のジェネリック型引数を使用する
        • <T, U>みたいな
  • enumもジェネリックにできる
    • これはResultOptionで履修済み
      enum Result<T, E> {
          Ok(T),
          Err(E),
      }
      
  • コード内で型のみが異なる構造体やenum定義が現れたら、ジェネリック型で重複を削減できる可能性を考慮しよう!
  • 構造体にメソッドを定義する際にもジェネリック型を使用できる
    struct Point<T> {
        x: T,
        y: T,
    }
    
    impl<T> Point<T> {
        fn x(&self) -> &T {
    	&self.x
        }
    }
    
    fn main() {
        let p = Point { x: 5, y: 10 };
    
        println!("p.x = {}", p.x());
    }
    
    • implの直後にTを宣言する必要がある
      • Point<T>にメソッドを実装している
      • 逆に、具体的な型を使用することもできる
        impl Point<f32> {
            fn distance_from_origin(&self) -> f32 {
        	(self.x.powi(2) + self.y.powi(2)).sqrt()
            }
        }
        
        • これは、Tがf32の時だけこのメソッドが定義されることを表す
  • ジェネリクスを使用していてもコードの実行は遅くならない!
    • コンパイル時に、使用されている箇所を全てチェックして、具体的な型のコードを生成する

トレイト

トレイト、一発で変換できながち(トレイと、になる)
interfaceかな?と思っている

  • 違いはあるけど大体インターフェースです!
  • traitキーワードで宣言
    • 一発で変換できないし今後はtraitと言うことにする
  • trait定義
    pub trait Summary {
        fn summarize(&self) -> String;
    }
    
  • traitの実装
    pub struct NewsArticle {
        pub headline: String,
        pub location: String,
        pub author: String,
        pub content: String,
    }
    
    impl Summary for NewsArticle {
        fn summarize(&self) -> String {
    	format!("{}, by {} ({})", self.headline, self.author, self.location)
        }
    }
    
    pub struct Tweet {
        pub username: String,
        pub content: String,
        pub reply: bool,
        pub retweet: bool,
    }
    
    impl Summary for Tweet {
        fn summarize(&self) -> String {
    	format!("{}: {}", self.username, self.content)
        }
    }
    
    • 通常のメソッド実装に似ているが、forキーワードと実装対象の型を記述する
      • forってループの予約語だと思うけど他の部分でも使うの不思議だ
  • traitのメソッドにデフォルトの振る舞いを定義できる
    pub trait Summary {
        fn summarize(&self) -> String {
    	// "(もっと読む)"
    	String::from("(Read more...)")
        }
    }
    
    • そのまま使うかOverrideするか選べる
      • そのまま使うなら空でまとめてあげれば良い
        • impl Summary for NewsArticle {}
      • Overrideするのも、普通に実装するのと同じでいい
    • デフォルト実装の中で、デフォルト実装を持たないメソッドを呼べる
      pub trait Summary {
          fn summarize_author(&self) -> String;
      
          fn summarize(&self) -> String {
      	// "({}さんの文章をもっと読む)"
      	format!("(Read more from {}...)", self.summarize_author())
          }
      }
      
      • 一部の振る舞いだけを型によって変えたいみたいな時めっちゃ便利そうだ。。
      • オーバーライドしている実装から、元のデフォルト実装を呼ぶことはできない
        • まあそれはそうか
  • 特定のtraitを実装している型を引数に指定する
    pub fn notify(item: &impl Summary) {
        println!("Breaking news! {}", item.summarize());
    }
    
    • 具体的な型の代わりにtrait名を指定して、それを実装しているあらゆる型を受け付ける
  • impl Trait構文は実はsyntax sugar
    • 元々は、トレイト境界(trait bound)と呼ばれる構文
      pub fn notify<T: Summary>(item: &T) {
          // 速報! {}
          println!("Breaking news! {}", item.summarize());
      }
      
      • 複雑な場面ではトレイト境界を用いて表現できる
      • 例としてSummaryを実装する2つのパラメータを持つケースを考える
        • impl Trait構文: pub fn notify(item1: &impl Summary, item2: &impl Summary) {
        • トレイト境界構文: pub fn notify<T: Summary>(item1: &T, item2: &T) {
        • この2つは意味が違っていて、impl Trait構文では2引数の型が異なっていても良い
        • 型が同一であることを強制するにはトレイト境界構文を使用するしかない
          • ジェネリクス使ってるので、同一の型になるということですね
  • 2つのTraitを実装していることを求めるには+構文を使う
    • pub fn notify(item: &(impl Summary + Display)) {
    • pub fn notify<T: Summary + Display>(item: &T) {
  • トレイト境界を複数使っていると、情報が多くなり読みにくくなる
    • それを解消するため、where句をサポートしている
    fn some_function<T: Display + Clone, U: Clone + Debug>(t: &T, u: &U) -> i32 {
    
    これを
    fn some_function<T, U>(t: &T, u: &U) -> i32
        where T: Display + Clone,
    	  U: Clone + Debug
    {
    
    こうじゃ
  • impl Trait構文を戻り値の型に使用する
    fn returns_summarizable() -> impl Summary {
        Tweet {
    	username: String::from("horse_ebooks"),
    	content: String::from(
    	    "of course, as you probably already know, people",
    	),
    	reply: false,
    	retweet: false,
        }
    }
    
    • Summaryを実装している型を返却する
    • これは一種類の型を返している時にしか使用できない
      • NewsArticleTweetを返すというのは許されない
        • これをする方法は後日!
  • トレイト境界を使用して、メソッド実装に条件を付与する
    use std::fmt::Display;
    
    struct Pair<T> {
        x: T,
        y: T,
    }
    
    impl<T> Pair<T> {
        fn new(x: T, y: T) -> Self {
    	Self { x, y }
        }
    }
    
    impl<T: Display + PartialOrd> Pair<T> {
        fn cmp_display(&self) {
    	if self.x >= self.y {
    	    println!("The largest member is x = {}", self.x);
    	} else {
    	    println!("The largest member is y = {}", self.y);
    	}
        }
    }
    
    • TがDisplayPartialOrdを実装している場合のみcmp_displayを実装する
    • ブランケット実装により、別のTraitを実装する全ての型に対するTrait実装を条件分けできる
      impl<T: Display> ToString for T {
          // --snip--
      }
      
      • 標準ライブラリはDisplayトレイトを実装するあらゆる型にToStringトレイトを実装している
      • 整数に対してto_stringメソッドを呼べるのはこのため

ライフタイム

スコープが生きてる期間、みたいなイメージ

ライフタイムとは、その参照が有効になるスコープのことです。

概念の理解としては合ってそう

  • 型が推論されるように、ライフタイムも大体の場合暗黙的に推論される
    • 型の選択肢が複数ある際に注釈をつけるように、ライフタイムにも注釈をつけるパターンもある
  • ここではライフタイムの全てを解説しません
    • 助かる〜
  • ライフタイムでダングリング参照を回避
    • ライフタイムの主な目的 -> 解放済みのメモリを参照するケースを回避する
  • ライフタイムをコンパイラが把握できないパターン
    fn main() {
        let string1 = String::from("abcd");
        let string2 = "xyz";
    
        let result = longest(string1.as_str(), string2);
        println!("The longest string is {}", result);
    }
    
    fn longest(x: &str, y: &str) -> &str {
        if x.len() > y.len() {
    	x
        } else {
    	y
        }
    }
    
    • ぱっと見コンパイル通りそうな感じもするが通らない
    • xとyのライフタイムがどう戻り値のライフタイムと関係するかわからない

ライフタイム注釈記法

  • ジェネリックなライフタイム引数を指定された関数は、あらゆるライフタイムの参照を受け取れる
  • ライフタイム引数の名称はアポストロフィー(')で始まり、全部小文字
    • 大体は'aとするらしい
&i32        // a reference
            // (ただの)参照
&'a i32     // a reference with an explicit lifetime
            // 明示的なライフタイム付きの参照
&'a mut i32 // a mutable reference with an explicit lifetime
            // 明示的なライフタイム付きの可変参照

関数シグニチャにおけるライフタイム注釈

  • longest関数に、全ての引数と戻り値が同じライフタイムを持つことを示す注釈を追加する
    fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
        if x.len() > y.len() {
    	x
        } else {
    	y
        }
    }
    
    • longest関数は、渡された参照のうち小さい方のライフタイムと同じライフタイムの参照を返す
  • 受け取る参照のライフタイムが戻り値のライフタイムに関与しないなら注釈は要らない

構造体定義のライフタイム注釈

  • 構造体に参照を保持させる場合、構造体定義の全参照にライフタイム注釈が必要
    struct ImportantExcerpt<'a> {
        part: &'a str,
    }
    
    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,
        };
    }
    
    • この構造体のインスタンスが、partフィールドに保持している参照より長生きしないことを意味する

ライフタイム省略

fn first_word(s: &str) -> &str {
    let bytes = s.as_bytes();

    for (i, &item) in bytes.iter().enumerate() {
        if item == b' ' {
            return &s[0..i];
        }
    }

    &s[..]
}
  • 4章で登場したこの関数がライフタイム注釈なしでコンパイルできたのな〜んでだ
    • Rust1.0以前では注釈が必要だった
      • fn first_word<'a>(s: &'a str) -> &'a str {
    • パターン化できるものについては計算できるようにし、注釈が不要になった
  • 下記の規則で計算できる場合は注釈が不要
    1. 参照である各引数は独自のライフタイム引数を得る
      a. 引数1つならライフタイム1つ
    2. ライフタイムが1つであるなら、全ての戻り値のライフタイムにそれが代入される
    3. 複数のライフタイム引数のうち、selfの参照が含まれれば、全ての戻り値のライフタイムにそれが代入される
  • 上記例は規則1,2により全てのライフタイムが計算できるため、注釈が不要になる

メソッド定義におけるライフタイム注釈

  • 構造体にライフタイムのあるメソッドを実装する際は、ジェネリック型引数と同じ記法を使用する
    impl<'a> ImportantExcerpt<'a> {
        fn level(&self) -> i32 {
    	3
        }
    }
    
    • impl後のライフタイム引数宣言と型名の後に付けるのは必須だが、省略規則によりselfには不要

静的ライフタイム

  • 'staticライフタイムという特殊なライフタイムが存在する
    • 参照がプログラムの全期間生存できることを意味する
      • 文字列リテラルはバイナリに直接格納されるので、全て'staticライフタイムを持つ
        • let s: &'static str = "I have a static lifetime.";
  • コンパイラから「'staticにしたらいいよ」と言われることがあるが、それが最適なのかよく考えましょう
  • global変数みたいなことかな?

全部登場する関数

fn main() {
    let string1 = String::from("abcd");
    let string2 = "xyz";

    let result = longest_with_an_announcement(
        string1.as_str(),
        string2,
        "Today is someone's birthday!",
    );
    println!("The longest string is {}", result);
}

use std::fmt::Display;

fn longest_with_an_announcement<'a, T>(
    x: &'a str,
    y: &'a str,
    ann: T,
) -> &'a str
where
    T: Display,
{
    //       "アナウンス! {}"
    println!("Announcement! {}", ann);
    if x.len() > y.len() {
        x
    } else {
        y
    }
}

本日のまとめ

Traitとライフタイムを1章でやるのおかしくない?(KONAMI)
少しずつ難解になっていっているが、まだギリわかる
明日は11章!テストについてとのこと
Rustは言語仕様にテストが組み込まれているらしいので楽しみ
ようやく折り返したぞ〜〜〜

Discussion