ch10-02-traits.md
commit 3c2ca8528c3b92b7d30e73f2e8a1b84b2f68b0c8
trait 告訴 Rust 編譯器某個特定類型擁有可能與其他類型共享的功能。可以通過 trait 以一種抽象的方式定義共享的行為??梢允褂?nbsp;trait bounds 指定泛型是任何擁有特定行為的類型。
注意:trait 類似于其他語言中的常被稱為 接口(interfaces)的功能,雖然有一些不同。
一個類型的行為由其可供調(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)在我們定義了 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)。
有時為 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 和在類型上實現(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
。
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
值的具體類型必須一致。
如果 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
。
然而,使用過多的 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ù)很像。
也可以在返回值中使用 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ù)。
現(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 的泛型參數(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í)生命周期是如何做到這些的。
更多建議: