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

Generic: Data Type & Traits & Lifetime

Comprehension

  • Generic form 1: Data Types

    // generic declaration - struct
    struct Point<T> {
        x: T,
        y: T,
    }
    let point = Point { x: 5, y: 4 }; // must be the same type
    struct PointDiffType<T, U> {
        x: T,
        y: U,
    }
    let point_diff_type = PointDiffType{ x: 5, y: 4.0 }; // different types
    
    // generic declaration - impl
    impl<T> Point<T> {
        fn x(&self) -> &T {
            &self.x
        }
    }
    
    // generic declaration - enum
    enum Option<T> {
        Some(T),
        None,
    }
    enum Result<T, E> {
        Ok(T),
        Err(E),
    }
    
    // a mixed implementation
    struct Point<T, U> {
        x: T,
        y: U,
    }
    impl<T, U> Point<T, U> {
    	// generic flexibility allows fields to be mixed to create a third generic instance
        // this syntax can be used for 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);
    }
    
  • Using generics does not affect runtime performance

    The monomorphization process converts generics to concrete types during compilation

    // for example, generic code using Some
    let integer = Some(5);
    let float = Some(5.0);
    // monomorphization produces the following code at compile time
    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);
    }
    
  • Generic form 2: Traits & Trait Bounds

    A trait describes shared behavior; in basic usage, it can be applied to structs, function parameters, and return values

    For a struct, a trait specifies the methods that the struct must provide, similar to a Java interface

    impl structA for traitA

    For a generic type, a trait specifies that its concrete type must have certain traits (also called trait bounds) to use a given set of functions

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

    For function parameters, traits restrict which arguments can be passed (polymorphism)

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

    For return values, a trait guarantees that the returned value implements it

    // 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 can call other impls
    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 types can specify trait bounds
    pub fn notify<T: Summary>(item1: &T, item2: &T) {}
    
    // multiple traits, case 1 - use the + operator
    pub fn notify(item: &(impl Summary + Display)) {
    pub fn notify<T: Summary + Display>(item: &T) {
    
    // multiple traits, case 2 - use 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 ensure that only generic types with the required trait have the 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);
            }
        }
    }
    
  • Generic form 3: Lifetimes

    &i32        // a reference
    &'a i32     // a reference with an explicit lifetime
    &'a mut i32 // a mutable reference with an explicit lifetime
    // generally used in function definitions
    fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
        if x.len() > y.len() {
            x
        } else {
            y
        }
    }
    // can be used with structs
    struct ImportantExcerpt<'a> {
        part: &'a str,
    }
    
    // lifetime elision - applying three rules
    fn first_word(s: &str) -> &str {...}
    
    // the static lifetime remains available for the entire program
    let s: &'static str = "I have a static lifetime.";
    

    The compiler uses three rules to infer lifetimes for references when they are not explicitly annotated

    The first rule is that each parameter that is a reference gets its own lifetime parameter.

    First: each reference parameter gets its own lifetime parameter.

    The second rule is if there is exactly one input lifetime parameter, that lifetime is assigned to all output lifetime parameters

    Second: if there is exactly one input lifetime parameter, it is assigned to all output lifetime parameters.

    The third rule is if there are multiple input lifetime parameters, but one of them is &self or &mut self because this is a method, the lifetime of self is assigned to all output lifetime parameters.

    Third: if there are multiple input lifetime parameters and one is &self or &mut self (a method), the lifetime of self is assigned to all output lifetime parameters.

  • Combining the three forms

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

Origin

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

…

Performance of Code Using Generics

You might be wondering whether there is a runtime cost when you’re using generic type parameters. The good news is that Rust implements generics in such a way that your code doesn’t run any slower using generic types than it would with concrete types.

Rust accomplishes this by performing monomorphization of the code that is using generics at compile time. Monomorphization is the process of turning generic code into specific code by filling in the concrete types that are used when compiled.

…

The compiler uses three rules to figure out what lifetimes references have when there aren’t explicit annotations. The first rule applies to input lifetimes, and the second and third rules apply to output lifetimes. If the compiler gets to the end of the three rules and there are still references for which it can’t figure out lifetimes, the compiler will stop with an error. These rules apply to fn definitions as well as impl blocks.

The first rule is that each parameter that is a reference gets its own 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.

The second rule is if there is exactly one input lifetime parameter, that lifetime is assigned to all output lifetime parameters: fn foo<'a>(x: &'a i32) -> &'a i32.

The third rule is if there are multiple input lifetime parameters, but one of them is &self or &mut self because this is a method, the lifetime of self is assigned to all output lifetime parameters. This third rule makes methods much nicer to read and write because fewer symbols are necessary.