Rust - Basic - 10 - Обобщённые типы, трейты и времена жизни

Дженерики: типы данных, трейты и времена жизни

Кратко

  • Способ 1: Data Type

    // объявление дженерика - 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) уже превращает дженерики в конкретные типы

    // например, код с дженериком Some
    let integer = Some(5);
    let float = Some(5.0);
    // на этапе компиляции произойдёт мономорфизация до следующего кода
    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 признак уточняет, что у этого struct есть объявленные в признаке method, аналогично interface в Java

    impl structA for traitA

    Для generic 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());
    }
    
    // можно указать trait bounds через generic type
    pub fn notify<T: Summary>(item1: &T, item2: &T) {}
    
    // несколько трейтов, случай 1 - через оператор +
    pub fn notify(item: &(impl Summary + Display)) {
    pub fn notify<T: Summary + Display>(item: &T) {
    
    // несколько трейтов, случай 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 можно сделать так: соответствующий method есть только у generic type с нужным трейтом
    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: Lifetimes

    &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 правила, чтобы вычислить времена жизни ссылок при отсутствии явных аннотаций

    Первое правило: каждый параметр, являющийся ссылкой, получает собственный параметр времени жизни.

    Первое правило: каждый входной параметр-ссылка имеет собственный параметр времени жизни.

    Второе правило: если входной параметр времени жизни ровно один, это время жизни присваивается всем выходным параметрам времени жизни

    Второе правило: если входной параметр времени жизни один, он присваивается всем выходным параметрам времени жизни.

    Третье правило: если входных параметров времени жизни несколько, но один из них — &self или &mut self, потому что это method, то время жизни self присваивается всем выходным параметрам времени жизни.

    Третье правило: если входных параметров времени жизни несколько и один из них — &self или &mut self (method), то параметр времени жизни self присваивается всем выходным параметрам времени жизни.

  • Совместное использование трёх способов

    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

…

Производительность кода с дженериками

Возможно, вас интересует, есть ли накладные расходы во время выполнения при использовании параметров обобщённого типа. Хорошая новость: Rust реализует дженерики так, что код с обобщёнными типами не работает медленнее, чем с конкретными.

Rust добивается этого, выполняя на этапе компиляции мономорфизацию кода, использующего дженерики. Мономорфизация — это процесс превращения обобщённого кода в конкретный путём подстановки конкретных типов, используемых при компиляции.

…

Компилятор использует три правила, чтобы определить, какие времена жизни имеют ссылки, когда явных аннотаций нет. Первое правило относится к входным временам жизни, второе и третье — к выходным. Если компилятор доходит до конца трёх правил, а для каких-то ссылок время жизни определить всё ещё не может, он останавливается с ошибкой. Эти правила применяются и к определениям fn, и к блокам impl.

Первое правило: каждый параметр-ссылка получает собственный параметр времени жизни. Иначе говоря, функция с одним параметром получает один параметр времени жизни: fn foo<'a>(x: &'a i32); функция с двумя параметрами получает два отдельных параметра времени жизни: fn foo<'a, 'b>(x: &'a i32, y: &'b i32) и так далее.

Второе правило: если входной параметр времени жизни ровно один, это время жизни присваивается всем выходным параметрам времени жизни: fn foo<'a>(x: &'a i32) -> &'a i32.

Третье правило: если входных параметров времени жизни несколько, но один из них — &self или &mut self, потому что это method, время жизни self присваивается всем выходным параметрам времени жизни. Это третье правило делает methods гораздо удобнее для чтения и записи: нужно меньше символов.