ch19-03-advanced-traits.md
commit 81d05c9a6d06d79f2a85c8ea184f41dc82532d98
第十章 “trait:定義共享的行為” 部分,我們第一次涉及到了 trait,不過就像生命周期一樣,我們并沒有覆蓋一些較為高級的細(xì)節(jié)?,F(xiàn)在我們更加了解 Rust 了,可以深入理解其本質(zhì)了。
關(guān)聯(lián)類型(associated types)是一個將類型占位符與 trait 相關(guān)聯(lián)的方式,這樣 trait 的方法簽名中就可以使用這些占位符類型。trait 的實(shí)現(xiàn)者會針對特定的實(shí)現(xiàn)在這個類型的位置指定相應(yīng)的具體類型。如此可以定義一個使用多種類型的 trait,直到實(shí)現(xiàn)此 trait 時都無需知道這些類型具體是什么。
本章所描述的大部分內(nèi)容都非常少見。關(guān)聯(lián)類型則比較適中;它們比本書其他的內(nèi)容要少見,不過比本章中的很多內(nèi)容要更常見。
一個帶有關(guān)聯(lián)類型的 trait 的例子是標(biāo)準(zhǔn)庫提供的 Iterator
trait。它有一個叫做 Item
的關(guān)聯(lián)類型來替代遍歷的值的類型。第十三章的 “Iterator trait 和 next 方法” 部分曾提到過 Iterator
trait 的定義如示例 19-12 所示:
pub trait Iterator {
type Item;
fn next(&mut self) -> Option<Self::Item>;
}
示例 19-12: Iterator
trait 的定義中帶有關(guān)聯(lián)類型 Item
Item
是一個占位類型,同時 next
方法定義表明它返回 Option<Self::Item>
類型的值。這個 trait 的實(shí)現(xiàn)者會指定 Item
的具體類型,然而不管實(shí)現(xiàn)者指定何種類型, next
方法都會返回一個包含了此具體類型值的 Option
。
關(guān)聯(lián)類型看起來像一個類似泛型的概念,因?yàn)樗试S定義一個函數(shù)而不指定其可以處理的類型。那么為什么要使用關(guān)聯(lián)類型呢?
讓我們通過一個在第十三章中出現(xiàn)的 Counter
結(jié)構(gòu)體上實(shí)現(xiàn) Iterator
trait 的例子來檢視其中的區(qū)別。在示例 13-21 中,指定了 Item
的類型為 u32
:
文件名: src/lib.rs
impl Iterator for Counter {
type Item = u32;
fn next(&mut self) -> Option<Self::Item> {
// --snip--
這類似于泛型。那么為什么 Iterator
trait 不像示例 19-13 那樣定義呢?
pub trait Iterator<T> {
fn next(&mut self) -> Option<T>;
}
示例 19-13: 一個使用泛型的 Iterator
trait 假想定義
區(qū)別在于當(dāng)如示例 19-13 那樣使用泛型時,則不得不在每一個實(shí)現(xiàn)中標(biāo)注類型。這是因?yàn)槲覀円部梢詫?shí)現(xiàn)為 Iterator<String> for Counter
,或任何其他類型,這樣就可以有多個 Counter
的 Iterator
的實(shí)現(xiàn)。換句話說,當(dāng) trait 有泛型參數(shù)時,可以多次實(shí)現(xiàn)這個 trait,每次需改變泛型參數(shù)的具體類型。接著當(dāng)使用 Counter
的 next
方法時,必須提供類型注解來表明希望使用 Iterator
的哪一個實(shí)現(xiàn)。
通過關(guān)聯(lián)類型,則無需標(biāo)注類型,因?yàn)椴荒芏啻螌?shí)現(xiàn)這個 trait。對于示例 19-12 使用關(guān)聯(lián)類型的定義,我們只能選擇一次 Item
會是什么類型,因?yàn)橹荒苡幸粋€ impl Iterator for Counter
。當(dāng)調(diào)用 Counter
的 next
時不必每次指定我們需要 u32
值的迭代器。
當(dāng)使用泛型類型參數(shù)時,可以為泛型指定一個默認(rèn)的具體類型。如果默認(rèn)類型就足夠的話,這消除了為具體類型實(shí)現(xiàn) trait 的需要。為泛型類型指定默認(rèn)類型的語法是在聲明泛型類型時使用 <PlaceholderType=ConcreteType>
。
這種情況的一個非常好的例子是用于運(yùn)算符重載。運(yùn)算符重載(Operator overloading)是指在特定情況下自定義運(yùn)算符(比如 +
)行為的操作。
Rust 并不允許創(chuàng)建自定義運(yùn)算符或重載任意運(yùn)算符,不過 std::ops
中所列出的運(yùn)算符和相應(yīng)的 trait 可以通過實(shí)現(xiàn)運(yùn)算符相關(guān) trait 來重載。例如,示例 19-14 中展示了如何在 Point
結(jié)構(gòu)體上實(shí)現(xiàn) Add
trait 來重載 +
運(yùn)算符,這樣就可以將兩個 Point
實(shí)例相加了:
文件名: src/main.rs
use std::ops::Add;
#[derive(Debug, Copy, Clone, PartialEq)]
struct Point {
x: i32,
y: i32,
}
impl Add for Point {
type Output = Point;
fn add(self, other: Point) -> Point {
Point {
x: self.x + other.x,
y: self.y + other.y,
}
}
}
fn main() {
assert_eq!(
Point { x: 1, y: 0 } + Point { x: 2, y: 3 },
Point { x: 3, y: 3 }
);
}
示例 19-14: 實(shí)現(xiàn) Add
trait 重載 Point
實(shí)例的 +
運(yùn)算符
add
方法將兩個 Point
實(shí)例的 x
值和 y
值分別相加來創(chuàng)建一個新的 Point
。Add
trait 有一個叫做 Output
的關(guān)聯(lián)類型,它用來決定 add
方法的返回值類型。
這里默認(rèn)泛型類型位于 Add
trait 中。這里是其定義:
trait Add<Rhs=Self> {
type Output;
fn add(self, rhs: Rhs) -> Self::Output;
}
這些代碼看來應(yīng)該很熟悉,這是一個帶有一個方法和一個關(guān)聯(lián)類型的 trait。比較陌生的部分是尖括號中的 Rhs=Self
:這個語法叫做 默認(rèn)類型參數(shù)(default type parameters)。Rhs
是一個泛型類型參數(shù)(“right hand side” 的縮寫),它用于定義 add
方法中的 rhs
參數(shù)。如果實(shí)現(xiàn) Add
trait 時不指定 Rhs
的具體類型,Rhs
的類型將是默認(rèn)的 Self
類型,也就是在其上實(shí)現(xiàn) Add
的類型。
當(dāng)為 Point
實(shí)現(xiàn) Add
時,使用了默認(rèn)的 Rhs
,因?yàn)槲覀兿M麑蓚€ Point
實(shí)例相加。讓我們看看一個實(shí)現(xiàn) Add
trait 時希望自定義 Rhs
類型而不是使用默認(rèn)類型的例子。
這里有兩個存放不同單元值的結(jié)構(gòu)體,Millimeters
和 Meters
。(這種將現(xiàn)有類型簡單封裝進(jìn)另一個結(jié)構(gòu)體的方式被稱為 newtype 模式(newtype pattern,之后的 “為了類型安全和抽象而使用 newtype 模式” 部分會詳細(xì)介紹。)我們希望能夠?qū)⒑撩字蹬c米值相加,并讓 Add
的實(shí)現(xiàn)正確處理轉(zhuǎn)換。可以為 Millimeters
實(shí)現(xiàn) Add
并以 Meters
作為 Rhs
,如示例 19-15 所示。
文件名: src/lib.rs
use std::ops::Add;
struct Millimeters(u32);
struct Meters(u32);
impl Add<Meters> for Millimeters {
type Output = Millimeters;
fn add(self, other: Meters) -> Millimeters {
Millimeters(self.0 + (other.0 * 1000))
}
}
示例 19-15: 在 Millimeters
上實(shí)現(xiàn) Add
,以便能夠?qū)?nbsp;Millimeters
與 Meters
相加
為了使 Millimeters
和 Meters
能夠相加,我們指定 impl Add<Meters>
來設(shè)定 Rhs
類型參數(shù)的值而不是使用默認(rèn)的 Self
。
默認(rèn)參數(shù)類型主要用于如下兩個方面:
標(biāo)準(zhǔn)庫的 Add
trait 就是一個第二個目的例子:大部分時候你會將兩個相似的類型相加,不過它提供了自定義額外行為的能力。在 Add
trait 定義中使用默認(rèn)類型參數(shù)意味著大部分時候無需指定額外的參數(shù)。換句話說,一小部分實(shí)現(xiàn)的樣板代碼是不必要的,這樣使用 trait 就更容易了。
第一個目的是相似的,但過程是反過來的:如果需要為現(xiàn)有 trait 增加類型參數(shù),為其提供一個默認(rèn)類型將允許我們在不破壞現(xiàn)有實(shí)現(xiàn)代碼的基礎(chǔ)上擴(kuò)展 trait 的功能。
Rust 既不能避免一個 trait 與另一個 trait 擁有相同名稱的方法,也不能阻止為同一類型同時實(shí)現(xiàn)這兩個 trait。甚至直接在類型上實(shí)現(xiàn)開始已經(jīng)有的同名方法也是可能的!
不過,當(dāng)調(diào)用這些同名方法時,需要告訴 Rust 我們希望使用哪一個。考慮一下示例 19-16 中的代碼,這里定義了 trait Pilot
和 Wizard
都擁有方法 fly
。接著在一個本身已經(jīng)實(shí)現(xiàn)了名為 fly
方法的類型 Human
上實(shí)現(xiàn)這兩個 trait。每一個 fly
方法都進(jìn)行了不同的操作:
文件名: src/main.rs
trait Pilot {
fn fly(&self);
}
trait Wizard {
fn fly(&self);
}
struct Human;
impl Pilot for Human {
fn fly(&self) {
println!("This is your captain speaking.");
}
}
impl Wizard for Human {
fn fly(&self) {
println!("Up!");
}
}
impl Human {
fn fly(&self) {
println!("*waving arms furiously*");
}
}
示例 19-16: 兩個 trait 定義為擁有 fly
方法,并在直接定義有 fly
方法的 Human
類型上實(shí)現(xiàn)這兩個 trait
當(dāng)調(diào)用 Human
實(shí)例的 fly
時,編譯器默認(rèn)調(diào)用直接實(shí)現(xiàn)在類型上的方法,如示例 19-17 所示。
文件名: src/main.rs
fn main() {
let person = Human;
person.fly();
}
示例 19-17: 調(diào)用 Human
實(shí)例的 fly
運(yùn)行這段代碼會打印出 *waving arms furiously*
,這表明 Rust 調(diào)用了直接實(shí)現(xiàn)在 Human
上的 fly
方法。
為了能夠調(diào)用 Pilot
trait 或 Wizard
trait 的 fly
方法,我們需要使用更明顯的語法以便能指定我們指的是哪個 fly
方法。這個語法展示在示例 19-18 中:
文件名: src/main.rs
fn main() {
let person = Human;
Pilot::fly(&person);
Wizard::fly(&person);
person.fly();
}
示例 19-18: 指定我們希望調(diào)用哪一個 trait 的 fly
方法
在方法名前指定 trait 名向 Rust 澄清了我們希望調(diào)用哪個 fly
實(shí)現(xiàn)。也可以選擇寫成 Human::fly(&person)
,這等同于示例 19-18 中的 person.fly()
,不過如果無需消歧義的話這么寫就有點(diǎn)長了。
運(yùn)行這段代碼會打印出:
$ cargo run
Compiling traits-example v0.1.0 (file:///projects/traits-example)
Finished dev [unoptimized + debuginfo] target(s) in 0.46s
Running `target/debug/traits-example`
This is your captain speaking.
Up!
*waving arms furiously*
因?yàn)?nbsp;fly
方法獲取一個 self
參數(shù),如果有兩個 類型 都實(shí)現(xiàn)了同一 trait,Rust 可以根據(jù) self
的類型計(jì)算出應(yīng)該使用哪一個 trait 實(shí)現(xiàn)。
然而,關(guān)聯(lián)函數(shù)是 trait 的一部分,但沒有 self
參數(shù)。當(dāng)同一作用域的兩個類型實(shí)現(xiàn)了同一 trait,Rust 就不能計(jì)算出我們期望的是哪一個類型,除非使用 完全限定語法(fully qualified syntax)。例如,拿示例 19-19 中的 Animal
trait 來說,它有關(guān)聯(lián)函數(shù) baby_name
,結(jié)構(gòu)體 Dog
實(shí)現(xiàn)了 Animal
,同時有關(guān)聯(lián)函數(shù) baby_name
直接定義于 Dog
之上:
文件名: src/main.rs
trait Animal {
fn baby_name() -> String;
}
struct Dog;
impl Dog {
fn baby_name() -> String {
String::from("Spot")
}
}
impl Animal for Dog {
fn baby_name() -> String {
String::from("puppy")
}
}
fn main() {
println!("A baby dog is called a {}", Dog::baby_name());
}
示例 19-19: 一個帶有關(guān)聯(lián)函數(shù)的 trait 和一個帶有同名關(guān)聯(lián)函數(shù)并實(shí)現(xiàn)了此 trait 的類型
這段代碼用于一個動物收容所,他們將所有的小狗起名為 Spot,這實(shí)現(xiàn)為定義于 Dog
之上的關(guān)聯(lián)函數(shù) baby_name
。Dog
類型還實(shí)現(xiàn)了 Animal
trait,它描述了所有動物的共有的特征。小狗被稱為 puppy,這表現(xiàn)為 Dog
的 Animal
trait 實(shí)現(xiàn)中與 Animal
trait 相關(guān)聯(lián)的函數(shù) baby_name
。
在 main
調(diào)用了 Dog::baby_name
函數(shù),它直接調(diào)用了定義于 Dog
之上的關(guān)聯(lián)函數(shù)。這段代碼會打印出:
$ cargo run
Compiling traits-example v0.1.0 (file:///projects/traits-example)
Finished dev [unoptimized + debuginfo] target(s) in 0.54s
Running `target/debug/traits-example`
A baby dog is called a Spot
這并不是我們需要的。我們希望調(diào)用的是 Dog
上 Animal
trait 實(shí)現(xiàn)那部分的 baby_name
函數(shù),這樣能夠打印出 A baby dog is called a puppy
。示例 19-18 中用到的技術(shù)在這并不管用;如果將 main
改為示例 19-20 中的代碼,則會得到一個編譯錯誤:
文件名: src/main.rs
fn main() {
println!("A baby dog is called a {}", Animal::baby_name());
}
示例 19-20: 嘗試調(diào)用 Animal
trait 的 baby_name
函數(shù),不過 Rust 并不知道該使用哪一個實(shí)現(xiàn)
因?yàn)?nbsp;Animal::baby_name
是關(guān)聯(lián)函數(shù)而不是方法,因此它沒有 self
參數(shù),Rust 無法計(jì)算出所需的是哪一個 Animal::baby_name
實(shí)現(xiàn)。我們會得到這個編譯錯誤:
$ cargo run
Compiling traits-example v0.1.0 (file:///projects/traits-example)
error[E0283]: type annotations needed
--> src/main.rs:20:43
|
20 | println!("A baby dog is called a {}", Animal::baby_name());
| ^^^^^^^^^^^^^^^^^ cannot infer type
|
= note: cannot satisfy `_: Animal`
For more information about this error, try `rustc --explain E0283`.
error: could not compile `traits-example` due to previous error
為了消歧義并告訴 Rust 我們希望使用的是 Dog
的 Animal
實(shí)現(xiàn),需要使用 完全限定語法,這是調(diào)用函數(shù)時最為明確的方式。示例 19-21 展示了如何使用完全限定語法:
文件名: src/main.rs
fn main() {
println!("A baby dog is called a {}", <Dog as Animal>::baby_name());
}
示例 19-21: 使用完全限定語法來指定我們希望調(diào)用的是 Dog
上 Animal
trait 實(shí)現(xiàn)中的 baby_name
函數(shù)
我們在尖括號中向 Rust 提供了類型注解,并通過在此函數(shù)調(diào)用中將 Dog
類型當(dāng)作 Animal
對待,來指定希望調(diào)用的是 Dog
上 Animal
trait 實(shí)現(xiàn)中的 baby_name
函數(shù)?,F(xiàn)在這段代碼會打印出我們期望的數(shù)據(jù):
$ cargo run
Compiling traits-example v0.1.0 (file:///projects/traits-example)
Finished dev [unoptimized + debuginfo] target(s) in 0.48s
Running `target/debug/traits-example`
A baby dog is called a puppy
通常,完全限定語法定義為:
<Type as Trait>::function(receiver_if_method, next_arg, ...);
對于關(guān)聯(lián)函數(shù),其沒有一個 receiver
,故只會有其他參數(shù)的列表??梢赃x擇在任何函數(shù)或方法調(diào)用處使用完全限定語法。然而,允許省略任何 Rust 能夠從程序中的其他信息中計(jì)算出的部分。只有當(dāng)存在多個同名實(shí)現(xiàn)而 Rust 需要幫助以便知道我們希望調(diào)用哪個實(shí)現(xiàn)時,才需要使用這個較為冗長的語法。
有時我們可能會需要某個 trait 使用另一個 trait 的功能。在這種情況下,需要能夠依賴相關(guān)的 trait 也被實(shí)現(xiàn)。這個所需的 trait 是我們實(shí)現(xiàn)的 trait 的 父(超) trait(supertrait)。
例如我們希望創(chuàng)建一個帶有 outline_print
方法的 trait OutlinePrint
,它會打印出帶有星號框的值。也就是說,如果 Point
實(shí)現(xiàn)了 Display
并返回 (x, y)
,調(diào)用以 1
作為 x
和 3
作為 y
的 Point
實(shí)例的 outline_print
會顯示如下:
**********
* *
* (1, 3) *
* *
**********
在 outline_print
的實(shí)現(xiàn)中,因?yàn)橄M軌蚴褂?nbsp;Display
trait 的功能,則需要說明 OutlinePrint
只能用于同時也實(shí)現(xiàn)了 Display
并提供了 OutlinePrint
需要的功能的類型??梢酝ㄟ^在 trait 定義中指定 OutlinePrint: Display
來做到這一點(diǎn)。這類似于為 trait 增加 trait bound。示例 19-22 展示了一個 OutlinePrint
trait 的實(shí)現(xiàn):
文件名: src/main.rs
use std::fmt;
trait OutlinePrint: fmt::Display {
fn outline_print(&self) {
let output = self.to_string();
let len = output.len();
println!("{}", "*".repeat(len + 4));
println!("*{}*", " ".repeat(len + 2));
println!("* {} *", output);
println!("*{}*", " ".repeat(len + 2));
println!("{}", "*".repeat(len + 4));
}
}
示例 19-22: 實(shí)現(xiàn) OutlinePrint
trait,它要求來自 Display
的功能
因?yàn)橹付?nbsp;OutlinePrint
需要 Display
trait,則可以在 outline_print
中使用 to_string
, 其會為任何實(shí)現(xiàn) Display
的類型自動實(shí)現(xiàn)。如果不在 trait 名后增加 : Display
并嘗試在 outline_print
中使用 to_string
,則會得到一個錯誤說在當(dāng)前作用域中沒有找到用于 &Self
類型的方法 to_string
。
讓我們看看如果嘗試在一個沒有實(shí)現(xiàn) Display
的類型上實(shí)現(xiàn) OutlinePrint
會發(fā)生什么,比如 Point
結(jié)構(gòu)體:
文件名: src/main.rs
struct Point {
x: i32,
y: i32,
}
impl OutlinePrint for Point {}
這樣會得到一個錯誤說 Display
是必須的而未被實(shí)現(xiàn):
$ cargo run
Compiling traits-example v0.1.0 (file:///projects/traits-example)
error[E0277]: `Point` doesn't implement `std::fmt::Display`
--> src/main.rs:20:6
|
20 | impl OutlinePrint for Point {}
| ^^^^^^^^^^^^ `Point` cannot be formatted with the default formatter
|
= help: the trait `std::fmt::Display` is not implemented for `Point`
= note: in format strings you may be able to use `{:?}` (or {:#?} for pretty-print) instead
note: required by a bound in `OutlinePrint`
--> src/main.rs:3:21
|
3 | trait OutlinePrint: fmt::Display {
| ^^^^^^^^^^^^ required by this bound in `OutlinePrint`
For more information about this error, try `rustc --explain E0277`.
error: could not compile `traits-example` due to previous error
一旦在 Point
上實(shí)現(xiàn) Display
并滿足 OutlinePrint
要求的限制,比如這樣:
文件名: src/main.rs
use std::fmt;
impl fmt::Display for Point {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "({}, {})", self.x, self.y)
}
}
那么在 Point
上實(shí)現(xiàn) OutlinePrint
trait 將能成功編譯,并可以在 Point
實(shí)例上調(diào)用 outline_print
來顯示位于星號框中的點(diǎn)的值。
在第十章的 “為類型實(shí)現(xiàn) trait” 部分,我們提到了孤兒規(guī)則(orphan rule),它說明只要 trait 或類型對于當(dāng)前 crate 是本地的話就可以在此類型上實(shí)現(xiàn)該 trait。一個繞開這個限制的方法是使用 newtype 模式(newtype pattern),它涉及到在一個元組結(jié)構(gòu)體(第五章 “用沒有命名字段的元組結(jié)構(gòu)體來創(chuàng)建不同的類型” 部分介紹了元組結(jié)構(gòu)體)中創(chuàng)建一個新類型。這個元組結(jié)構(gòu)體帶有一個字段作為希望實(shí)現(xiàn) trait 的類型的簡單封裝。接著這個封裝類型對于 crate 是本地的,這樣就可以在這個封裝上實(shí)現(xiàn) trait。Newtype 是一個源自 (U.C.0079,逃) Haskell 編程語言的概念。使用這個模式?jīng)]有運(yùn)行時性能懲罰,這個封裝類型在編譯時就被省略了。
例如,如果想要在 Vec<T>
上實(shí)現(xiàn) Display
,而孤兒規(guī)則阻止我們直接這么做,因?yàn)?nbsp;Display
trait 和 Vec<T>
都定義于我們的 crate 之外??梢詣?chuàng)建一個包含 Vec<T>
實(shí)例的 Wrapper
結(jié)構(gòu)體,接著可以如列表 19-23 那樣在 Wrapper
上實(shí)現(xiàn) Display
并使用 Vec<T>
的值:
文件名: src/main.rs
use std::fmt;
struct Wrapper(Vec<String>);
impl fmt::Display for Wrapper {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "[{}]", self.0.join(", "))
}
}
fn main() {
let w = Wrapper(vec![String::from("hello"), String::from("world")]);
println!("w = {}", w);
}
示例 19-23: 創(chuàng)建 Wrapper
類型封裝 Vec<String>
以便能夠?qū)崿F(xiàn) Display
Display
的實(shí)現(xiàn)使用 self.0
來訪問其內(nèi)部的 Vec<T>
,因?yàn)?nbsp;Wrapper
是元組結(jié)構(gòu)體而 Vec<T>
是結(jié)構(gòu)體總位于索引 0 的項(xiàng)。接著就可以使用 Wrapper
中 Display
的功能了。
此方法的缺點(diǎn)是,因?yàn)?nbsp;Wrapper
是一個新類型,它沒有定義于其值之上的方法;必須直接在 Wrapper
上實(shí)現(xiàn) Vec<T>
的所有方法,這樣就可以代理到self.0
上 —— 這就允許我們完全像 Vec<T>
那樣對待 Wrapper
。如果希望新類型擁有其內(nèi)部類型的每一個方法,為封裝類型實(shí)現(xiàn) Deref
trait(第十五章 “通過 Deref trait 將智能指針當(dāng)作常規(guī)引用處理” 部分討論過)并返回其內(nèi)部類型是一種解決方案。如果不希望封裝類型擁有所有內(nèi)部類型的方法 —— 比如為了限制封裝類型的行為 —— 則必須只自行實(shí)現(xiàn)所需的方法。
上面便是 newtype 模式如何與 trait 結(jié)合使用的;還有一個不涉及 trait 的實(shí)用模式。現(xiàn)在讓我們將話題的焦點(diǎn)轉(zhuǎn)移到一些與 Rust 類型系統(tǒng)交互的高級方法上來吧。
更多建議: