1. 程式人生 > 實用技巧 >Rust Lang Book Ch.10 Generic Types, Traits. and Lifetimes

Rust Lang Book Ch.10 Generic Types, Traits. and Lifetimes

泛型

在函式中

fn largest<T>(list: &[T]) -> &T {
    let mut largest = list[0];

    for item in list {
        if item > largest {
            largest = item;
        }
    }

    largest
}

  

在struct中

struct Point<T> {
    x: T,
    y: T,
}

fn main() {
    let integer = Point { x: 5, y: 10 };//注意如果是{x: 5, y: 4.0},那麼就不可能編譯,因為編譯器無法推測T到底是int還是float
    let float = Point { x: 1.0, y: 4.0 };
}

  

在enum中:

enum Result<T, E> {
    Ok(T),
    Err(E),
}

  

在method中:

struct Point<T> {
    x: T,
    y: T,
}

impl<T> Point<T> {//注意要在impl之後就宣告一次T
    fn x(&self) -> &T {
        &self.x
    }
}

fn main() {
    let p = Point { x: 5, y: 10 };

    println!("p.x = {}", p.x());
}

  

能夠僅僅對泛型中的其中一種具現化提供實現

impl Point<f32> {
    fn distance_from_origin(&self) -> f32 {
        (self.x.powi(2) + self.y.powi(2)).sqrt()
    }
}

  

struct和函式可以使用不同的泛型列表,例如struct本身使用T, U,方法可以使用W, V

impl<T, U> Point<T, U> {
    fn mixup<V, W>(self, other: Point<V, W>) -> Point<T, W> {
        Point {
            x: self.x,
            y: other.y,
        }
    }
}

  

在Rust中,使用泛型的代價是幾乎沒有的。因為Rust會在編譯的時候對泛型做單態化(Monomorphization),為每個泛型所對應的具體實際型別生成對應的程式碼。例如,對應Some(5)和Some(5.0),編譯器會識別到i32和f64都是Option<T>對應的具體型別,因此會生成Option_i32和Option_f64兩個enum並完善對應邏輯。

Trait-特性

類似於其他語言的介面。一個trait內可以宣告多個函式簽名,這些函式在實現了之後就可以像正常成員函式一樣呼叫

pub trait Summary {
    fn summarize(&self) -> String;//注意這裡僅僅提供函式簽名。同時要注意這裡的成員變數也是要加&self的
//如果這裡提供了具體邏輯,就會成為預設實現。

然後再為每種type實現這些trait。與普通的實現不同,這裡要在impl之後寫對應trait的名字+for。注意,要為了某個型別實現trait具體邏輯,需要這個trait或者這個型別有一方是當前crate中的。例如,可以為Vec<T>實現Summary trait,但是不能為Vec<T>實現Display trait,這一限制被成為orphan rule,是內聚性的一種體現。

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

  

在trait內部直接提供函式具體邏輯,就會成為預設邏輯。為具體型別實現trait的時候就不需要為具體型別提供邏輯,可以直接放一個空的impl scope,例如impl Summary for NewsArticle {}

pub trait Summary {
    fn summarize(&self) -> String {//預設實現
        String::from("(Read more...)")
    }
}

  

trait的函式能夠呼叫trait的其他函式,因此,合理搭配預設邏輯和需要override的邏輯能夠起到事倍功半的效果。

pub trait Summary {
    fn summarize_author(&self) -> String;

    fn summarize(&self) -> String {
        format!("(Read more from {}...)", self.summarize_author())
    }
}

  

impl Summary for Tweet {
    fn summarize_author(&self) -> String {
        format!("@{}", self.username)
    }
}

  

    let tweet = Tweet {
        username: String::from("horse_ebooks"),
        content: String::from(
            "of course, as you probably already know, people",
        ),
        reply: false,
        retweet: false,
    };

    println!("1 new tweet: {}", tweet.summarize());

  

Trait作為函式引數

trait可以作為函式引數,但是需要搭配impl關鍵字或者Trait Bound Syntax一同使用。

impl關鍵字

pub fn notify(item: &impl Summary) {//只有實現了Summary的型別才能作為引數接受
    println!("Breaking news! {}", item.summarize());
}

  

Trait Bound Syntax:搭配泛型使用

pub fn notify<T: Summary>(item: &T) {
    println!("Breaking news! {}", item.summarize());
}

  

用+可以要求引數同時滿足多個Traits

pub fn notify(item: &(impl Summary + Display)) {

  

pub fn notify<T: Summary + Display>(item: &T) {

  

Trait Bounds加上+加上where語句也可以完成這一任務,而且更加清晰:

fn some_function<T, U>(t: &T, u: &U) -> i32
    where T: Display + Clone,
          U: Clone + Debug
{

  

Trait作為返回值

可以使用impl Trait語法來返回實現了具體trait的型別示例。但是,return impl Trait要求你的函式只可能返回一種型別,比如NewArticle和Tweet都實現了Summary trait,想要根據情況返回NewArticle或者Tweet就是不行的。

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

  

如下所示,return impl TraitName只能返回單一的型別,否則就會報錯: expected Struct XXX, found struct YYY

fn returns_summarizable(switch: bool) -> impl Summary {
    if switch {
        NewsArticle {
            headline: String::from(
                "Penguins win the Stanley Cup Championship!",
            ),
            location: String::from("Pittsburgh, PA, USA"),
            author: String::from("Iceburgh"),
            content: String::from(
                "The Pittsburgh Penguins once again are the best \
                 hockey team in the NHL.",
            ),
        }
    } else {
        Tweet {
            username: String::from("horse_ebooks"),
            content: String::from(
                "of course, as you probably already know, people",
            ),
            reply: false,
            retweet: false,
        }
         | |_|_________^ expected struct `NewsArticle`, found struct `Tweet`
    }
     |   |_____- `if` and `else` have incompatible types
}

  

Trait作為其他Trait的條件之一

實現一個滿足了某個trait bounds對應的型別上面的新trait稱為blanket implementation

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

  

Lifetimes

引用的生命週期一般是通過推斷得到的,但是也可以有額外的註解來自定義生命週期。

Rust使用Borrow Checker來檢查變數的生命週期,並且要求引用的生命週期一定要被覆蓋在對應變數的生命週期之內,否則就報錯。

    {
        let r;                // ---------+-- 'a
                              //          |
        {                     //          |
            let x = 5;        // -+-- 'b  |
            r = &x;           //  |       |
        }                     // -+       |
                              //          |
        println!("r: {}", r); //          |
    }                         // ---------+

  

當遇到Borrow Checker無法確定生命週期的情況時,即引用進入一個函式再從一個函式返回時,編譯器會直接報錯。

fn longest(x: &str, y: &str) -> &str {
                                 ^ expected lifetime parameter
				//報錯的原因是Borrow Checker不知道返回的究竟是x還是y,因而無法追蹤引用的生命週期
    if x.len() > y.len() {
        x
    } else {
        y
    }
}

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

    let result = longest(string1.as_str(), string2);
    println!("The longest string is {}", result);
}

  

此時需要告訴編譯器引用的生命週期,加'a即為生命週期註解,註解本身沒有什麼意義,可以認為是在限制多個引用的生命週期的關係不會相互影響。

&i32        // a reference
&'a i32     // a reference with an explicit lifetime
&'a mut i32 // a mutable reference with an explicit lifetime

  

Borrow Checker將拒絕不滿足annotation的引用作為引數。

fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
//這裡表示引數x,y和返回值的生命週期是必須一致的
//在實際上,這表示返回的生命週期至少要和x,y中生命週期最短的一個一樣長
//x, y生命週期最短的那個必須覆蓋返回值的生命週期
if x.len() > y.len() { x } else { y } }

  

fn main() {
    let string1 = String::from("long string is long");
    let result;
    {
        let string2 = String::from("xyz");
        result = longest(string1.as_str(), string2.as_str());
		                                    ^^^^^^^ borrowed value does not live long enough
    }
    println!("The longest string is {}", result);
}

  

如果返回值僅僅與其中一個引數有關,那就只需要宣告這個引數與返回值生命週期的關係。

fn longest<'a>(x: &'a str, y: &str) -> &'a str {
    x
}

  

如果宣告和實際關係不符,編譯器會報錯:

1 | fn longest<'a>(x: &'a str, y: &str) -> &'a str {
  |                               ---- help: add explicit lifetime `'a` to the type of `y`: `&'a str`
2 |     y
  |     ^ lifetime `'a` required

  

Struct和Lifetime Annotation

對於成員變數是引用的情況,也可以新增lifetime annotation,但是需要注意所有的引用成員變數都要註明同樣的生命週期。

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

  

Lifetime Elision

有的時候無需用'a註明生命週期,這主要是Rust編譯器對某些應用場景會自動標上生命週期,這被成為lifetime elision rules。當然,Rust編譯器如果猜不出引用的生命週期就會報錯了,而這時候就需要程式設計師來標註。

函式或者成員函式的引數被稱為input lifetimes,返回值則被稱為output lifetimes。

首先,編譯器為每個引數分配一個lifetime parameters,比如'a, 'b, 'c,以此類推,例如fn foo<'a>(x: &'a i32)和fn foo<'a, 'b>(x: &'a i32, y: &'b i32)。接著,如果只有一個input lifetime parameter,那麼所有返回值中的引用的生命週期都與這個引數相同。例如fn foo<'a>(x: &'a i32) -> &'a i32。最後,如果存在多個input lifetime parameters,但是其中一個是&self,那麼編譯器自動設定所有output lifetime parameters與&self或者&mut self的生命週期相同。

所以,對於fn first_word(s: &str) -> &str {這樣一個函式,編譯器能夠自動設定生命週期的關聯。

impl<'a> ImportantExcerpt<'a> {
    fn announce_and_return_part(&self, announcement: &str) -> &str {
        println!("Attention please: {}", announcement);
        self.part//因為一個input lifetime parameter是self,所以這裡設定返回值的生命週期和&self繫結。
    }
}

  

The Static Lifetime

這裡靜態生命週期指的是在整個程式執行期間,都必須要有效。

let s: &'static str = "I have a static lifetime.";