Rust - Grundlagen - 10 - Generische Datentypen, Traits und Lebenszeiten

Generische Datentypen, Traits und Lebenszeiten

Verständnis

  • Generics, Variante 1: Datentypen

    // 泛型声明 - 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);
    }
    
  • Die Verwendung von Generics beeinflusst die Laufzeitleistung nicht

    Denn der Monomorphisierungsprozess wandelt Generics bereits beim Kompilieren in konkrete Typen um.

    // 比如使用了 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);
    }
    
  • Generics, Variante 2: Traits und Trait Bounds

    Ein Trait beschreibt eine Eigenschaft. Grundsätzlich kann er Structs, Funktionsparameter und Rückgabewerte mit einer Eigenschaft versehen.

    Bei einem Struct kann der Trait festlegen, dass es die darin deklarierten Methoden besitzt, ähnlich wie ein Java-interface.

    impl structA for traitA

    Bei einem generischen Typ kann der Trait festlegen, dass nur konkrete Typen mit diesen Eigenschaften (Trait Bounds) die entsprechende Methodensammlung erhalten.

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

    Bei Funktionsparametern kann der Trait einschränken, dass nur Parameter mit den entsprechenden Eigenschaften verwendet werden dürfen (Polymorphie).

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

    Bei einem Rückgabewert kann der Trait garantieren, dass der Rückgabewert diese Eigenschaft besitzt.

    // 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);
            }
        }
    }
    
  • Generics, Variante 3: Lebenszeiten

    &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.";
    

    Der Compiler verwendet drei Regeln, um Lebenszeiten von Referenzen ohne explizite Annotationen zu bestimmen.

    Die erste Regel besagt, dass jeder Referenzparameter seinen eigenen Lebenszeitparameter erhält.

    Erste Regel: Jeder Referenzparameter erhält einen eigenen Lebenszeitparameter.

    Die zweite Regel besagt: Gibt es genau einen Eingabe-Lebenszeitparameter, wird diese Lebenszeit allen Ausgabe-Lebenszeitparametern zugewiesen.

    Zweite Regel: Bei genau einem Eingabe-Lebenszeitparameter wird dieser allen Ausgabe-Lebenszeitparametern zugewiesen.

    Die dritte Regel besagt: Gibt es mehrere Eingabe-Lebenszeitparameter und ist einer davon wegen einer Methode &self oder &mut self, wird die Lebenszeit von self allen Ausgabe-Lebenszeitparametern zugewiesen.

    Dritte Regel: Bei mehreren Eingabe-Lebenszeitparametern wird die Lebenszeit von self allen Ausgaben zugewiesen, wenn einer &self oder &mut self ist.

  • Kombination der drei Varianten

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

Quelle

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

…

Leistung von Code mit Generics

Vielleicht fragst du dich, ob die Verwendung generischer Typparameter Laufzeitkosten verursacht. Die gute Nachricht: Rust implementiert Generics so, dass Code mit generischen Typen nicht langsamer läuft als Code mit konkreten Typen.

Rust erreicht dies durch Monomorphisierung des generischen Codes zur Kompilierzeit. Monomorphisierung bedeutet, generischen Code in spezifischen Code umzuwandeln, indem beim Kompilieren die verwendeten konkreten Typen eingesetzt werden.

…

Der Compiler verwendet drei Regeln, um Lebenszeiten von Referenzen ohne explizite Annotationen zu bestimmen. Die erste Regel betrifft Eingabe-Lebenszeiten, die zweite und dritte Ausgabe-Lebenszeiten. Wenn nach allen drei Regeln noch Referenzen mit unbestimmter Lebenszeit übrig bleiben, bricht der Compiler mit einem Fehler ab. Die Regeln gelten sowohl für fn-Definitionen als auch für impl-Blöcke.

Die erste Regel: Jeder Referenzparameter erhält einen eigenen Lebenszeitparameter. Eine Funktion mit einem Parameter erhält also einen Parameter: fn foo<'a>(x: &'a i32); eine Funktion mit zwei Parametern erhält zwei getrennte Parameter: fn foo<'a, 'b>(x: &'a i32, y: &'b i32); und so weiter.

Die zweite Regel: Gibt es genau einen Eingabe-Lebenszeitparameter, wird diese Lebenszeit allen Ausgabe-Lebenszeitparametern zugewiesen: fn foo<'a>(x: &'a i32) -> &'a i32.

Die dritte Regel: Gibt es mehrere Eingabe-Lebenszeitparameter und ist einer davon &self oder &mut self, weil es sich um eine Methode handelt, wird die Lebenszeit von self allen Ausgabe-Lebenszeitparametern zugewiesen. Dadurch sind Methoden angenehmer zu lesen und zu schreiben, weil weniger Symbole nötig sind.