Rust Trait:定義共同行為

2023-03-22 15:10 更新
ch10-02-traits.md
commit 3c2ca8528c3b92b7d30e73f2e8a1b84b2f68b0c8

trait 告訴 Rust 編譯器某個特定類型擁有可能與其他類型共享的功能。可以通過 trait 以一種抽象的方式定義共享的行為??梢允褂?nbsp;trait bounds 指定泛型是任何擁有特定行為的類型。

注意:trait 類似于其他語言中的常被稱為 接口interfaces)的功能,雖然有一些不同。

定義 trait

一個類型的行為由其可供調(diào)用的方法構(gòu)成。如果可以對不同類型調(diào)用相同的方法的話,這些類型就可以共享相同的行為了。trait 定義是一種將方法簽名組合起來的方法,目的是定義一個實現(xiàn)某些目的所必需的行為的集合。

例如,這里有多個存放了不同類型和屬性文本的結(jié)構(gòu)體:結(jié)構(gòu)體 NewsArticle 用于存放發(fā)生于世界各地的新聞故事,而結(jié)構(gòu)體 Tweet 最多只能存放 280 個字符的內(nèi)容,以及像是否轉(zhuǎn)推或是否是對推友的回復(fù)這樣的元數(shù)據(jù)。

我們想要創(chuàng)建一個名為 aggregator 的多媒體聚合庫用來顯示可能儲存在 NewsArticle 或 Tweet 實例中的數(shù)據(jù)的總結(jié)。每一個結(jié)構(gòu)體都需要的行為是他們是能夠被總結(jié)的,這樣的話就可以調(diào)用實例的 summarize 方法來請求總結(jié)。示例 10-12 中展示了一個表現(xiàn)這個概念的公有 Summary trait 的定義:

文件名: src/lib.rs

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

示例 10-12:Summary trait 定義,它包含由 summarize 方法提供的行為

這里使用 trait 關(guān)鍵字來聲明一個 trait,后面是 trait 的名字,在這個例子中是 Summary。我們也聲明 trait 為 pub 以便依賴這個 crate 的 crate 也可以使用這個 trait,正如我們見過的一些示例一樣。在大括號中聲明描述實現(xiàn)這個 trait 的類型所需要的行為的方法簽名,在這個例子中是 fn summarize(&self) -> String。

在方法簽名后跟分號,而不是在大括號中提供其實現(xiàn)。接著每一個實現(xiàn)這個 trait 的類型都需要提供其自定義行為的方法體,編譯器也會確保任何實現(xiàn) Summary trait 的類型都擁有與這個簽名的定義完全一致的 summarize 方法。

trait 體中可以有多個方法:一行一個方法簽名且都以分號結(jié)尾。

為類型實現(xiàn) trait

現(xiàn)在我們定義了 Summary trait 的簽名,接著就可以在多媒體聚合庫中實現(xiàn)這個類型了。示例 10-13 中展示了 NewsArticle 結(jié)構(gòu)體上 Summary trait 的一個實現(xiàn),它使用標(biāo)題、作者和創(chuàng)建的位置作為 summarize 的返回值。對于 Tweet 結(jié)構(gòu)體,我們選擇將 summarize 定義為用戶名后跟推文的全部文本作為返回值,并假設(shè)推文內(nèi)容已經(jīng)被限制為 280 字符以內(nèi)。

文件名: src/lib.rs

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

示例 10-13:在 NewsArticle 和 Tweet 類型上實現(xiàn) Summary trait

在類型上實現(xiàn) trait 類似于實現(xiàn)與 trait 無關(guān)的方法。區(qū)別在于 impl 關(guān)鍵字之后,我們提供需要實現(xiàn) trait 的名稱,接著是 for 和需要實現(xiàn) trait 的類型的名稱。在 impl 塊中,使用 trait 定義中的方法簽名,不過不再后跟分號,而是需要在大括號中編寫函數(shù)體來為特定類型實現(xiàn) trait 方法所擁有的行為。

現(xiàn)在庫在 NewsArticle 和 Tweet 上實現(xiàn)了Summary trait,crate 的用戶可以像調(diào)用常規(guī)方法一樣調(diào)用 NewsArticle 和 Tweet 實例的 trait 方法了。唯一的區(qū)別是 trait 必須和類型一起引入作用域以便使用額外的 trait 方法。這是一個二進制 crate 如何利用 aggregator 庫 crate 的例子:

use aggregator::{Summary, Tweet};

fn main() {
    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());
}

這會打印出 1 new tweet: horse_ebooks: of course, as you probably already know, people

其他依賴 aggregator crate 的 crate 也可以將 Summary 引入作用域以便為其自己的類型實現(xiàn)該 trait。實現(xiàn) trait 時需要注意的一個限制是,只有當(dāng)至少一個 trait 或者要實現(xiàn) trait 的類型位于 crate 的本地作用域時,才能為該類型實現(xiàn) trait。例如,可以為 aggregator crate 的自定義類型 Tweet 實現(xiàn)如標(biāo)準庫中的 Display trait,這是因為 Tweet 類型位于 aggregator crate 本地的作用域中。類似地,也可以在 aggregator crate 中為 Vec<T> 實現(xiàn) Summary,這是因為 Summary trait 位于 aggregator crate 本地作用域中。

但是不能為外部類型實現(xiàn)外部 trait。例如,不能在 aggregator crate 中為 Vec<T> 實現(xiàn) Display trait。這是因為 Display 和 Vec<T> 都定義于標(biāo)準庫中,它們并不位于 aggregator crate 本地作用域中。這個限制是被稱為 相干性coherence) 的程序?qū)傩缘囊徊糠郑蛘吒唧w的說是 孤兒規(guī)則orphan rule),其得名于不存在父類型。這條規(guī)則確保了其他人編寫的代碼不會破壞你代碼,反之亦然。沒有這條規(guī)則的話,兩個 crate 可以分別對相同類型實現(xiàn)相同的 trait,而 Rust 將無從得知應(yīng)該使用哪一個實現(xiàn)。

默認實現(xiàn)

有時為 trait 中的某些或全部方法提供默認的行為,而不是在每個類型的每個實現(xiàn)中都定義自己的行為是很有用的。這樣當(dāng)為某個特定類型實現(xiàn) trait 時,可以選擇保留或重載每個方法的默認行為。

示例 10-14 中展示了如何為 Summary trait 的 summarize 方法指定一個默認的字符串值,而不是像示例 10-12 中那樣只是定義方法簽名:

文件名: src/lib.rs

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

示例 10-14:Summary trait 的定義,帶有一個 summarize 方法的默認實現(xiàn)

如果想要對 NewsArticle 實例使用這個默認實現(xiàn),而不是定義一個自己的實現(xiàn),則可以通過 impl Summary for NewsArticle {} 指定一個空的 impl 塊。

雖然我們不再直接為 NewsArticle 定義 summarize 方法了,但是我們提供了一個默認實現(xiàn)并且指定 NewsArticle 實現(xiàn) Summary trait。因此,我們?nèi)匀豢梢詫?nbsp;NewsArticle 實例調(diào)用 summarize 方法,如下所示:

    let article = 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.",
        ),
    };

    println!("New article available! {}", article.summarize());

這段代碼會打印 New article available! (Read more...)。

為 summarize 創(chuàng)建默認實現(xiàn)并不要求對示例 10-13 中 Tweet 上的 Summary 實現(xiàn)做任何改變。其原因是重載一個默認實現(xiàn)的語法與實現(xiàn)沒有默認實現(xiàn)的 trait 方法的語法一樣。

默認實現(xiàn)允許調(diào)用相同 trait 中的其他方法,哪怕這些方法沒有默認實現(xiàn)。如此,trait 可以提供很多有用的功能而只需要實現(xiàn)指定一小部分內(nèi)容。例如,我們可以定義 Summary trait,使其具有一個需要實現(xiàn)的 summarize_author 方法,然后定義一個 summarize 方法,此方法的默認實現(xiàn)調(diào)用 summarize_author 方法:

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

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

為了使用這個版本的 Summary,只需在實現(xiàn) trait 時定義 summarize_author 即可:

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

一旦定義了 summarize_author,我們就可以對 Tweet 結(jié)構(gòu)體的實例調(diào)用 summarize 了,而 summarize 的默認實現(xiàn)會調(diào)用我們提供的 summarize_author 定義。因為實現(xiàn)了 summarize_author,Summary trait 就提供了 summarize 方法的功能,且無需編寫更多的代碼。

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

這會打印出 1 new tweet: (Read more from @horse_ebooks...)

注意無法從相同方法的重載實現(xiàn)中調(diào)用默認方法。

trait 作為參數(shù)

知道了如何定義 trait 和在類型上實現(xiàn)這些 trait 之后,我們可以探索一下如何使用 trait 來接受多種不同類型的參數(shù)。

例如在示例 10-13 中為 NewsArticle 和 Tweet 類型實現(xiàn)了 Summary trait。我們可以定義一個函數(shù) notify 來調(diào)用其參數(shù) item 上的 summarize 方法,該參數(shù)是實現(xiàn)了 Summary trait 的某種類型。為此可以使用 impl Trait 語法,像這樣:

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

對于 item 參數(shù),我們指定了 impl 關(guān)鍵字和 trait 名稱,而不是具體的類型。該參數(shù)支持任何實現(xiàn)了指定 trait 的類型。在 notify 函數(shù)體中,可以調(diào)用任何來自 Summary trait 的方法,比如 summarize。我們可以傳遞任何 NewsArticle 或 Tweet 的實例來調(diào)用 notify。任何用其它如 String 或 i32 的類型調(diào)用該函數(shù)的代碼都不能編譯,因為它們沒有實現(xiàn) Summary。

Trait Bound 語法

impl Trait 語法適用于直觀的例子,它實際上是一種較長形式語法的語法糖。我們稱為 trait bound,它看起來像:

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

這與之前的例子相同,不過稍微冗長了一些。trait bound 與泛型參數(shù)聲明在一起,位于尖括號中的冒號后面。

impl Trait 很方便,適用于短小的例子。trait bound 則適用于更復(fù)雜的場景。例如,可以獲取兩個實現(xiàn)了 Summary 的參數(shù)。使用 impl Trait 的語法看起來像這樣:

pub fn notify(item1: &impl Summary, item2: &impl Summary) {

這適用于 item1 和 item2 允許是不同類型的情況(只要它們都實現(xiàn)了 Summary)。不過如果你希望強制它們都是相同類型呢?這只有在使用 trait bound 時才有可能:

pub fn notify<T: Summary>(item1: &T, item2: &T) {

泛型 T 被指定為 item1 和 item2 的參數(shù)限制,如此傳遞給參數(shù) item1 和 item2 值的具體類型必須一致。

通過 + 指定多個 trait bound

如果 notify 需要顯示 item 的格式化形式,同時也要使用 summarize 方法,那么 item 就需要同時實現(xiàn)兩個不同的 trait:Display 和 Summary。這可以通過 + 語法實現(xiàn):

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

+ 語法也適用于泛型的 trait bound:

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

通過指定這兩個 trait bound,notify 的函數(shù)體可以調(diào)用 summarize 并使用 {} 來格式化 item。

通過 where 簡化 trait bound

然而,使用過多的 trait bound 也有缺點。每個泛型有其自己的 trait bound,所以有多個泛型參數(shù)的函數(shù)在名稱和參數(shù)列表之間會有很長的 trait bound 信息,這使得函數(shù)簽名難以閱讀。為此,Rust 有另一個在函數(shù)簽名之后的 where 從句中指定 trait bound 的語法。所以除了這么寫:

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

還可以像這樣使用 where 從句:

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

這個函數(shù)簽名就顯得不那么雜亂,函數(shù)名、參數(shù)列表和返回值類型都離得很近,看起來跟沒有那么多 trait bounds 的函數(shù)很像。

返回實現(xiàn)了 trait 的類型

也可以在返回值中使用 impl Trait 語法,來返回實現(xiàn)了某個 trait 的類型:

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

通過使用 impl Summary 作為返回值類型,我們指定了 returns_summarizable 函數(shù)返回某個實現(xiàn)了 Summary trait 的類型,但是不確定其具體的類型。在這個例子中 returns_summarizable 返回了一個 Tweet,不過調(diào)用方并不知情。

返回一個只是指定了需要實現(xiàn)的 trait 的類型的能力在閉包和迭代器場景十分的有用,第十三章會介紹它們。閉包和迭代器創(chuàng)建只有編譯器知道的類型,或者是非常非常長的類型。impl Trait 允許你簡單的指定函數(shù)返回一個 Iterator 而無需寫出實際的冗長的類型。

不過這只適用于返回單一類型的情況。例如,這段代碼的返回值類型指定為返回 impl Summary,但是返回了 NewsArticle 或 Tweet 就行不通:

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

這里嘗試返回 NewsArticle 或 Tweet。這不能編譯,因為 impl Trait 工作方式的限制。第十七章的 “為使用不同類型的值而設(shè)計的 trait 對象” 部分會介紹如何編寫這樣一個函數(shù)。

使用 trait bounds 來修復(fù) largest 函數(shù)

現(xiàn)在你知道了如何使用泛型參數(shù) trait bound 來指定所需的行為。讓我們回到實例 10-5 修復(fù)使用泛型類型參數(shù)的 largest 函數(shù)定義!回顧一下,最后嘗試編譯代碼時出現(xiàn)的錯誤是:

$ cargo run
   Compiling chapter10 v0.1.0 (file:///projects/chapter10)
error[E0369]: binary operation `>` cannot be applied to type `T`
 --> src/main.rs:5:17
  |
5 |         if item > largest {
  |            ---- ^ ------- T
  |            |
  |            T
  |
help: consider restricting type parameter `T`
  |
1 | fn largest<T: std::cmp::PartialOrd>(list: &[T]) -> T {
  |             ++++++++++++++++++++++

For more information about this error, try `rustc --explain E0369`.
error: could not compile `chapter10` due to previous error

在 largest 函數(shù)體中我們想要使用大于運算符(>)比較兩個 T 類型的值。這個運算符被定義為標(biāo)準庫中 trait std::cmp::PartialOrd 的一個默認方法。所以需要在 T 的 trait bound 中指定 PartialOrd,這樣 largest 函數(shù)可以用于任何可以比較大小的類型的 slice。因為 PartialOrd 位于 prelude 中所以并不需要手動將其引入作用域。將 largest 的簽名修改為如下:

fn largest<T: PartialOrd>(list: &[T]) -> T {

但是如果編譯代碼的話,會出現(xiàn)一些不同的錯誤:

$ cargo run
   Compiling chapter10 v0.1.0 (file:///projects/chapter10)
error[E0508]: cannot move out of type `[T]`, a non-copy slice
 --> src/main.rs:2:23
  |
2 |     let mut largest = list[0];
  |                       ^^^^^^^
  |                       |
  |                       cannot move out of here
  |                       move occurs because `list[_]` has type `T`, which does not implement the `Copy` trait
  |                       help: consider borrowing here: `&list[0]`

error[E0507]: cannot move out of a shared reference
 --> src/main.rs:4:18
  |
4 |     for &item in list {
  |         -----    ^^^^
  |         ||
  |         |data moved here
  |         |move occurs because `item` has type `T`, which does not implement the `Copy` trait
  |         help: consider removing the `&`: `item`

Some errors have detailed explanations: E0507, E0508.
For more information about an error, try `rustc --explain E0507`.
error: could not compile `chapter10` due to 2 previous errors

錯誤的核心是 cannot move out of type [T], a non-copy slice,對于非泛型版本的 largest 函數(shù),我們只嘗試了尋找最大的 i32 和 char。正如第四章 “只在棧上的數(shù)據(jù):拷貝” 部分討論過的,像 i32 和 char 這樣的類型是已知大小的并可以儲存在棧上,所以他們實現(xiàn)了 Copy trait。當(dāng)我們將 largest 函數(shù)改成使用泛型后,現(xiàn)在 list 參數(shù)的類型就有可能是沒有實現(xiàn) Copy trait 的。這意味著我們可能不能將 list[0] 的值移動到 largest 變量中,這導(dǎo)致了上面的錯誤。

為了只對實現(xiàn)了 Copy 的類型調(diào)用這些代碼,可以在 T 的 trait bounds 中增加 Copy!示例 10-15 中展示了一個可以編譯的泛型版本的 largest 函數(shù)的完整代碼,只要傳遞給 largest 的 slice 值的類型實現(xiàn)了 PartialOrd  Copy 這兩個 trait,例如 i32 和 char

文件名: src/main.rs

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

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

    largest
}

fn main() {
    let number_list = vec![34, 50, 25, 100, 65];

    let result = largest(&number_list);
    println!("The largest number is {}", result);

    let char_list = vec!['y', 'm', 'a', 'q'];

    let result = largest(&char_list);
    println!("The largest char is {}", result);
}

示例 10-15:一個可以用于任何實現(xiàn)了 PartialOrd 和 Copy trait 的泛型的 largest 函數(shù)

如果并不希望限制 largest 函數(shù)只能用于實現(xiàn)了 Copy trait 的類型,我們可以在 T 的 trait bounds 中指定 Clone 而不是 Copy。并克隆 slice 的每一個值使得 largest 函數(shù)擁有其所有權(quán)。使用 clone 函數(shù)意味著對于類似 String 這樣擁有堆上數(shù)據(jù)的類型,會潛在的分配更多堆上空間,而堆分配在涉及大量數(shù)據(jù)時可能會相當(dāng)緩慢。

另一種 largest 的實現(xiàn)方式是返回在 slice 中 T 值的引用。如果我們將函數(shù)返回值從 T 改為 &T 并改變函數(shù)體使其能夠返回一個引用,我們將不需要任何 Clone 或 Copy 的 trait bounds 而且也不會有任何的堆分配。嘗試自己實現(xiàn)這種替代解決方式吧!如果你無法擺脫與生命周期有關(guān)的錯誤,請繼續(xù)閱讀:接下來的 “生命周期與引用有效性” 部分會詳細的說明,不過生命周期對于解決這些挑戰(zhàn)來說并不是必須的。

使用 trait bound 有條件地實現(xiàn)方法

通過使用帶有 trait bound 的泛型參數(shù)的 impl 塊,可以有條件地只為那些實現(xiàn)了特定 trait 的類型實現(xiàn)方法。例如,示例 10-16 中的類型 Pair<T> 總是實現(xiàn)了 new 方法并返回一個 Pair<T> 的實例(回憶一下第五章的 "定義方法" 部分,Self 是一個 impl 塊類型的類型別名(type alias),在這里是 Pair<T>)。不過在下一個 impl 塊中,只有那些為 T 類型實現(xiàn)了 PartialOrd trait (來允許比較)  Display trait (來啟用打?。┑?nbsp;Pair<T> 才會實現(xiàn) cmp_display 方法:

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

示例 10-16:根據(jù) trait bound 在泛型上有條件的實現(xiàn)方法

也可以對任何實現(xiàn)了特定 trait 的類型有條件地實現(xiàn) trait。對任何滿足特定 trait bound 的類型實現(xiàn) trait 被稱為 blanket implementations,他們被廣泛的用于 Rust 標(biāo)準庫中。例如,標(biāo)準庫為任何實現(xiàn)了 Display trait 的類型實現(xiàn)了 ToString trait。這個 impl 塊看起來像這樣:

impl<T: Display> ToString for T {
    // --snip--
}

因為標(biāo)準庫有了這些 blanket implementation,我們可以對任何實現(xiàn)了 Display trait 的類型調(diào)用由 ToString 定義的 to_string 方法。例如,可以將整型轉(zhuǎn)換為對應(yīng)的 String 值,因為整型實現(xiàn)了 Display

let s = 3.to_string();

blanket implementation 會出現(xiàn)在 trait 文檔的 “Implementers” 部分。

trait 和 trait bound 讓我們使用泛型類型參數(shù)來減少重復(fù),并仍然能夠向編譯器明確指定泛型類型需要擁有哪些行為。因為我們向編譯器提供了 trait bound 信息,它就可以檢查代碼中所用到的具體類型是否提供了正確的行為。在動態(tài)類型語言中,如果我們嘗試調(diào)用一個類型并沒有實現(xiàn)的方法,會在運行時出現(xiàn)錯誤。Rust 將這些錯誤移動到了編譯時,甚至在代碼能夠運行之前就強迫我們修復(fù)錯誤。另外,我們也無需編寫運行時檢查行為的代碼,因為在編譯時就已經(jīng)檢查過了,這樣相比其他那些不愿放棄泛型靈活性的語言有更好的性能。

這里還有一種泛型,我們一直在使用它甚至都沒有察覺它的存在,這就是 生命周期lifetimes)。不同于其他泛型幫助我們確保類型擁有期望的行為,生命周期則有助于確保引用在我們需要他們的時候一直有效。讓我們學(xué)習(xí)生命周期是如何做到這些的。


以上內(nèi)容是否對您有幫助:
在線筆記
App下載
App下載

掃描二維碼

下載編程獅App

公眾號
微信公眾號

編程獅公眾號