Rust - Basic - 10 - Generaic Data Type & Traits & Lifetime

ジェネリック: データ型・trait・ライフタイム

理解

  • ジェネリックの表現 1: データ型

    // 泛型声明 - struct
    struct Point<T> {
        x: T,
        y: T,
    }
    let point = Point { x: 5, y: 4 }; // 必须是同一类型
    struct PointDiffType<T, U> {
        x: T,
        y: U,
    }
    let point_diff_type = PointDiffType{ x: 5, y: 4.0 }; // 不同类型
    
    // 泛型声明 - impl
    impl<T> Point<T> {
        fn x(&self) -> &T {
            &self.x
        }
    }
    
    // 泛型声明 - enum
    enum Option<T> {
        Some(T),
        None,
    }
    enum Result<T, E> {
        Ok(T),
        Err(E),
    }
    
    // 一种 mix-up 实现
    struct Point<T, U> {
        x: T,
        y: U,
    }
    impl<T, U> Point<T, U> {
    		// 由于泛型的高灵活性,这里做了巧妙的 field 交叉混合,组合出第三个新泛型实例
        // 这种语法可用于 middleware
        fn mixup<V, W>(self, other: Point<V, W>) -> Point<T, W> {
            Point {
                x: self.x,
                y: other.y,
            }
        }
    }
    fn main() {
        let p1 = Point { x: 5, y: 10.4 };
        let p2 = Point { x: "Hello", y: 'c' };
        let p3 = p1.mixup(p2);
        println!("p3.x = {}, p3.y = {}", p3.x, p3.y);
    }
    
  • ジェネリックを使っても実行時性には影響しない

    コンパイル段階で Monomorphization process がジェネリックを具体的な型へ変換するためです。

    // 比如使用了 Some 泛型代码
    let integer = Some(5);
    let float = Some(5.0);
    // 编译期会 Monomorphization 为以下代码
    enum Option_i32 {
        Some(i32),
        None,
    }
    enum Option_f64 {
        Some(f64),
        None,
    }
    fn main() {
        let integer = Option_i32::Some(5);
        let float = Option_f64::Some(5.0);
    }
    
  • ジェネリックの表現 2: Traits と Trait Bounds

    Trait は「特性」を意味します。基本的な使い方では、struct、関数引数、関数の戻り値に特性を付けられます。

    struct では、その特性で宣言した method を持つことを明示できます。Java の interface に似ています。

    impl structA for traitA

    generic type では、対応する concrete type がこれらの特性(trait bound とも呼ばれます)を持つ場合だけ、対応する関数群を持てると示せます。

    impl<T: traitA + traitB + ...> structA<T> {...}

    関数引数では、対応する特性を持つ引数だけを受け取れるよう制限できます(多態性)。

    pub fn notify(item: &impl Summary) {...}

    関数の戻り値では、戻り値が必ずその特性を持つことを明示できます。

    // Defining a Trait
    pub trait Summary {
        fn summarize(&self) -> String;
    }
    
    // Implementing a Trait on a Type
    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)
        }
    }
    
    // Default Implementations
    pub trait Summary {
        fn summarize(&self) -> String {
            String::from("(Read more...)")
        }
    }
    
    // Default Implementations 可以调用其它 impl
    pub trait Summary {
        fn summarize_author(&self) -> String;
    
        fn summarize(&self) -> String {
            format!("(Read more from {}...)", self.summarize_author())
        }
    }
    
    // Traits as Parameters
    pub fn notify(item: &impl Summary) {
        println!("Breaking news! {}", item.summarize());
    }
    
    // 可以使用 generic type 来指定 trait bounds
    pub fn notify<T: Summary>(item1: &T, item2: &T) {}
    
    // 多个 trait 的情况 1 - 使用 + 操作符
    pub fn notify(item: &(impl Summary + Display)) {
    pub fn notify<T: Summary + Display>(item: &T) {
    
    // 多个 trait 的情况 2 - 使用 where
    fn some_function<T, U>(t: &T, u: &U) -> i32
        where T: Display + Clone,
              U: Clone + Debug
    
    // Returning Types that Implement Traits
    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,
        }
    }
    
    // 可以通过 trait bounds 特性,以实现:有对应 trait 的 generic type,才能有对应的 method
    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);
            }
        }
    }
    
  • ジェネリックの表現 3: ライフタイム

    &i32        // a reference
    &'a i32     // a reference with an explicit lifetime
    &'a mut i32 // a mutable reference with an explicit lifetime
    // 一般用在 function 定义时
    fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
        if x.len() > y.len() {
            x
        } else {
            y
        }
    }
    // 可用于 struct
    struct ImportantExcerpt<'a> {
        part: &'a str,
    }
    
    // Lifetime Elision - 活用 3 条规则
    fn first_word(s: &str) -> &str {...}
    
    // static 生命期,可以在整个程序运行时保持可用
    let s: &'static str = "I have a static lifetime.";
    

    コンパイラーは、明示的な注釈がない場合、3 つのルールで reference の lifetime を計算します。

    1 のルールは、reference である各パラメーターが固有の lifetime parameter を持つことです。

    1 条: reference の各入力パラメーターには独の lifetime parameter があります。

    2 のルールは、入力 lifetime parameter が 1 つだけなら、その lifetime をすべての出力 lifetime parameter に割りてることです。

    2 条: 入力 lifetime parameter が 1 つだけなら、それをすべての出力 lifetime parameter に割りてます。

    3 のルールは、入力 lifetime parameter が複数あり、そのうち 1 つが method の &self または &mut self なら、self の lifetime をすべての出力 lifetime parameter に割りてることです。

    3 条: 入力 lifetime parameter が複数あり、その 1 つが &self または &mut self(method)なら、self の lifetime parameter をすべての出力 lifetime parameter に割りてます。

  • 3 つの表現を組み合わせて使う

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

参照元

https://doc.rust-lang.org/book/ch10-00-generics.html

…

ジェネリックを使うコードのパフォーマンス

generic type parameter を使うと実行時コストが発生するのでは、と疑問に思うかもしれません。Rust のジェネリック実装では、generic type を使っても concrete type の場合よりコードが遅くなることはありません。

Rust は、ジェネリックを使うコードをコンパイル時に monomorphization することでこれを実現します。Monomorphization とは、コンパイル時に使われる concrete type を埋め、generic code を具体的なコードへ変換する処理です。

…

コンパイラーは、明示的な注釈がない場合、3 つのルールで reference の lifetime を判断します。 1 のルールは入力 lifetime、 2 と 3 のルールは出力 lifetime に適されます。3 つのルールを適しても lifetime を判断できない reference が残ると、コンパイラーはエラーで停止します。これらのルールは fn 定義と impl ブロックの両方に適されます。

1 のルールは、reference である各パラメーターが固有の lifetime parameter を持つことです。 In other words, a function with one parameter gets one lifetime parameter: fn foo<'a>(x: &'a i32); a function with two parameters gets two separate lifetime parameters: fn foo<'a, 'b>(x: &'a i32, y: &'b i32); and so on.

2 のルールは、入力 lifetime parameter が 1 つだけなら、その lifetime をすべての出力 lifetime parameter に割りてることです。: fn foo<'a>(x: &'a i32) -> &'a i32.

3 のルールは、入力 lifetime parameter が複数あり、そのうち 1 つが method の &self または &mut self なら、self の lifetime をすべての出力 lifetime parameter に割りてることです。This third rule makes methods much nicer to read and write because fewer symbols are necessary.