Rust 高級 trait

2023-03-22 15:16 更新
ch19-03-advanced-traits.md
commit 81d05c9a6d06d79f2a85c8ea184f41dc82532d98

第十章 “trait:定義共享的行為” 部分,我們第一次涉及到了 trait,不過就像生命周期一樣,我們并沒有覆蓋一些較為高級的細節(jié)?,F(xiàn)在我們更加了解 Rust 了,可以深入理解其本質(zhì)了。

關(guān)聯(lián)類型在 trait 定義中指定占位符類型

關(guān)聯(lián)類型associated types)是一個將類型占位符與 trait 相關(guān)聯(lián)的方式,這樣 trait 的方法簽名中就可以使用這些占位符類型。trait 的實現(xiàn)者會針對特定的實現(xiàn)在這個類型的位置指定相應的具體類型。如此可以定義一個使用多種類型的 trait,直到實現(xiàn)此 trait 時都無需知道這些類型具體是什么。

本章所描述的大部分內(nèi)容都非常少見。關(guān)聯(lián)類型則比較適中;它們比本書其他的內(nèi)容要少見,不過比本章中的很多內(nèi)容要更常見。

一個帶有關(guān)聯(lián)類型的 trait 的例子是標準庫提供的 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 的實現(xiàn)者會指定 Item 的具體類型,然而不管實現(xiàn)者指定何種類型, next 方法都會返回一個包含了此具體類型值的 Option。

關(guān)聯(lián)類型看起來像一個類似泛型的概念,因為它允許定義一個函數(shù)而不指定其可以處理的類型。那么為什么要使用關(guān)聯(lián)類型呢?

讓我們通過一個在第十三章中出現(xiàn)的 Counter 結(jié)構(gòu)體上實現(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ū)別在于當如示例 19-13 那樣使用泛型時,則不得不在每一個實現(xiàn)中標注類型。這是因為我們也可以實現(xiàn)為 Iterator<String> for Counter,或任何其他類型,這樣就可以有多個 Counter 的 Iterator 的實現(xiàn)。換句話說,當 trait 有泛型參數(shù)時,可以多次實現(xiàn)這個 trait,每次需改變泛型參數(shù)的具體類型。接著當使用 Counter 的 next 方法時,必須提供類型注解來表明希望使用 Iterator 的哪一個實現(xiàn)。

通過關(guān)聯(lián)類型,則無需標注類型,因為不能多次實現(xiàn)這個 trait。對于示例 19-12 使用關(guān)聯(lián)類型的定義,我們只能選擇一次 Item 會是什么類型,因為只能有一個 impl Iterator for Counter。當調(diào)用 Counter 的 next 時不必每次指定我們需要 u32 值的迭代器。

默認泛型類型參數(shù)和運算符重載

當使用泛型類型參數(shù)時,可以為泛型指定一個默認的具體類型。如果默認類型就足夠的話,這消除了為具體類型實現(xiàn) trait 的需要。為泛型類型指定默認類型的語法是在聲明泛型類型時使用 <PlaceholderType=ConcreteType>。

這種情況的一個非常好的例子是用于運算符重載。運算符重載Operator overloading)是指在特定情況下自定義運算符(比如 +)行為的操作。

Rust 并不允許創(chuàng)建自定義運算符或重載任意運算符,不過 std::ops 中所列出的運算符和相應的 trait 可以通過實現(xiàn)運算符相關(guān) trait 來重載。例如,示例 19-14 中展示了如何在 Point 結(jié)構(gòu)體上實現(xiàn) Add trait 來重載 + 運算符,這樣就可以將兩個 Point 實例相加了:

文件名: 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: 實現(xiàn) Add trait 重載 Point 實例的 + 運算符

add 方法將兩個 Point 實例的 x 值和 y 值分別相加來創(chuàng)建一個新的 PointAdd trait 有一個叫做 Output 的關(guān)聯(lián)類型,它用來決定 add 方法的返回值類型。

這里默認泛型類型位于 Add trait 中。這里是其定義:

trait Add<Rhs=Self> {
    type Output;

    fn add(self, rhs: Rhs) -> Self::Output;
}

這些代碼看來應該很熟悉,這是一個帶有一個方法和一個關(guān)聯(lián)類型的 trait。比較陌生的部分是尖括號中的 Rhs=Self:這個語法叫做 默認類型參數(shù)default type parameters)。Rhs 是一個泛型類型參數(shù)(“right hand side” 的縮寫),它用于定義 add 方法中的 rhs 參數(shù)。如果實現(xiàn) Add trait 時不指定 Rhs 的具體類型,Rhs 的類型將是默認的 Self 類型,也就是在其上實現(xiàn) Add 的類型。

當為 Point 實現(xiàn) Add 時,使用了默認的 Rhs,因為我們希望將兩個 Point 實例相加。讓我們看看一個實現(xiàn) Add trait 時希望自定義 Rhs 類型而不是使用默認類型的例子。

這里有兩個存放不同單元值的結(jié)構(gòu)體,Millimeters 和 Meters。(這種將現(xiàn)有類型簡單封裝進另一個結(jié)構(gòu)體的方式被稱為 newtype 模式newtype pattern,之后的 “為了類型安全和抽象而使用 newtype 模式” 部分會詳細介紹。)我們希望能夠?qū)⒑撩字蹬c米值相加,并讓 Add 的實現(xiàn)正確處理轉(zhuǎn)換??梢詾?nbsp;Millimeters 實現(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 上實現(xiàn) Add,以便能夠?qū)?nbsp;Millimeters 與 Meters 相加

為了使 Millimeters 和 Meters 能夠相加,我們指定 impl Add<Meters> 來設定 Rhs 類型參數(shù)的值而不是使用默認的 Self。

默認參數(shù)類型主要用于如下兩個方面:

  • 擴展類型而不破壞現(xiàn)有代碼。
  • 在大部分用戶都不需要的特定情況進行自定義。

標準庫的 Add trait 就是一個第二個目的例子:大部分時候你會將兩個相似的類型相加,不過它提供了自定義額外行為的能力。在 Add trait 定義中使用默認類型參數(shù)意味著大部分時候無需指定額外的參數(shù)。換句話說,一小部分實現(xiàn)的樣板代碼是不必要的,這樣使用 trait 就更容易了。

第一個目的是相似的,但過程是反過來的:如果需要為現(xiàn)有 trait 增加類型參數(shù),為其提供一個默認類型將允許我們在不破壞現(xiàn)有實現(xiàn)代碼的基礎上擴展 trait 的功能。

完全限定語法與消歧義:調(diào)用相同名稱的方法

Rust 既不能避免一個 trait 與另一個 trait 擁有相同名稱的方法,也不能阻止為同一類型同時實現(xiàn)這兩個 trait。甚至直接在類型上實現(xiàn)開始已經(jīng)有的同名方法也是可能的!

不過,當調(diào)用這些同名方法時,需要告訴 Rust 我們希望使用哪一個??紤]一下示例 19-16 中的代碼,這里定義了 trait Pilot 和 Wizard 都擁有方法 fly。接著在一個本身已經(jīng)實現(xiàn)了名為 fly 方法的類型 Human 上實現(xiàn)這兩個 trait。每一個 fly 方法都進行了不同的操作:

文件名: 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 類型上實現(xiàn)這兩個 trait

當調(diào)用 Human 實例的 fly 時,編譯器默認調(diào)用直接實現(xiàn)在類型上的方法,如示例 19-17 所示。

文件名: src/main.rs

fn main() {
    let person = Human;
    person.fly();
}

示例 19-17: 調(diào)用 Human 實例的 fly

運行這段代碼會打印出 *waving arms furiously*,這表明 Rust 調(diào)用了直接實現(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 實現(xiàn)。也可以選擇寫成 Human::fly(&person),這等同于示例 19-18 中的 person.fly(),不過如果無需消歧義的話這么寫就有點長了。

運行這段代碼會打印出:

$ 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*

因為 fly 方法獲取一個 self 參數(shù),如果有兩個 類型 都實現(xiàn)了同一 trait,Rust 可以根據(jù) self 的類型計算出應該使用哪一個 trait 實現(xiàn)。

然而,關(guān)聯(lián)函數(shù)是 trait 的一部分,但沒有 self 參數(shù)。當同一作用域的兩個類型實現(xiàn)了同一 trait,Rust 就不能計算出我們期望的是哪一個類型,除非使用 完全限定語法fully qualified syntax)。例如,拿示例 19-19 中的 Animal trait 來說,它有關(guān)聯(lián)函數(shù) baby_name,結(jié)構(gòu)體 Dog 實現(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ù)并實現(xiàn)了此 trait 的類型

這段代碼用于一個動物收容所,他們將所有的小狗起名為 Spot,這實現(xiàn)為定義于 Dog 之上的關(guān)聯(lián)函數(shù) baby_name。Dog 類型還實現(xiàn)了 Animal trait,它描述了所有動物的共有的特征。小狗被稱為 puppy,這表現(xiàn)為 Dog 的 Animal trait 實現(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 實現(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 并不知道該使用哪一個實現(xiàn)

因為 Animal::baby_name 是關(guān)聯(lián)函數(shù)而不是方法,因此它沒有 self 參數(shù),Rust 無法計算出所需的是哪一個 Animal::baby_name 實現(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 實現(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 實現(xiàn)中的 baby_name 函數(shù)

我們在尖括號中向 Rust 提供了類型注解,并通過在此函數(shù)調(diào)用中將 Dog 類型當作 Animal 對待,來指定希望調(diào)用的是 Dog 上 Animal trait 實現(xiàn)中的 baby_name 函數(shù)。現(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 能夠從程序中的其他信息中計算出的部分。只有當存在多個同名實現(xiàn)而 Rust 需要幫助以便知道我們希望調(diào)用哪個實現(xiàn)時,才需要使用這個較為冗長的語法。

父 trait 用于在另一個 trait 中使用某 trait 的功能

有時我們可能會需要某個 trait 使用另一個 trait 的功能。在這種情況下,需要能夠依賴相關(guān)的 trait 也被實現(xiàn)。這個所需的 trait 是我們實現(xiàn)的 trait 的 父(超) traitsupertrait)。

例如我們希望創(chuàng)建一個帶有 outline_print 方法的 trait OutlinePrint,它會打印出帶有星號框的值。也就是說,如果 Point 實現(xiàn)了 Display 并返回 (x, y),調(diào)用以 1 作為 x 和 3 作為 y 的 Point 實例的 outline_print 會顯示如下:

**********
*        *
* (1, 3) *
*        *
**********

在 outline_print 的實現(xiàn)中,因為希望能夠使用 Display trait 的功能,則需要說明 OutlinePrint 只能用于同時也實現(xiàn)了 Display 并提供了 OutlinePrint 需要的功能的類型。可以通過在 trait 定義中指定 OutlinePrint: Display 來做到這一點。這類似于為 trait 增加 trait bound。示例 19-22 展示了一個 OutlinePrint trait 的實現(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: 實現(xiàn) OutlinePrint trait,它要求來自 Display 的功能

因為指定了 OutlinePrint 需要 Display trait,則可以在 outline_print 中使用 to_string, 其會為任何實現(xiàn) Display 的類型自動實現(xiàn)。如果不在 trait 名后增加 : Display 并嘗試在 outline_print 中使用 to_string,則會得到一個錯誤說在當前作用域中沒有找到用于 &Self 類型的方法 to_string。

讓我們看看如果嘗試在一個沒有實現(xiàn) Display 的類型上實現(xiàn) OutlinePrint 會發(fā)生什么,比如 Point 結(jié)構(gòu)體:

文件名: src/main.rs

struct Point {
    x: i32,
    y: i32,
}

impl OutlinePrint for Point {}

這樣會得到一個錯誤說 Display 是必須的而未被實現(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 上實現(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 上實現(xiàn) OutlinePrint trait 將能成功編譯,并可以在 Point 實例上調(diào)用 outline_print 來顯示位于星號框中的點的值。

newtype 模式用以在外部類型上實現(xiàn)外部 trait

在第十章的 “為類型實現(xiàn) trait” 部分,我們提到了孤兒規(guī)則(orphan rule),它說明只要 trait 或類型對于當前 crate 是本地的話就可以在此類型上實現(xiàn)該 trait。一個繞開這個限制的方法是使用 newtype 模式newtype pattern),它涉及到在一個元組結(jié)構(gòu)體(第五章 “用沒有命名字段的元組結(jié)構(gòu)體來創(chuàng)建不同的類型” 部分介紹了元組結(jié)構(gòu)體)中創(chuàng)建一個新類型。這個元組結(jié)構(gòu)體帶有一個字段作為希望實現(xiàn) trait 的類型的簡單封裝。接著這個封裝類型對于 crate 是本地的,這樣就可以在這個封裝上實現(xiàn) trait。Newtype 是一個源自 (U.C.0079,逃) Haskell 編程語言的概念。使用這個模式?jīng)]有運行時性能懲罰,這個封裝類型在編譯時就被省略了。

例如,如果想要在 Vec<T> 上實現(xiàn) Display,而孤兒規(guī)則阻止我們直接這么做,因為 Display trait 和 Vec<T> 都定義于我們的 crate 之外??梢詣?chuàng)建一個包含 Vec<T> 實例的 Wrapper 結(jié)構(gòu)體,接著可以如列表 19-23 那樣在 Wrapper 上實現(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 的實現(xiàn)使用 self.0 來訪問其內(nèi)部的 Vec<T>,因為 Wrapper 是元組結(jié)構(gòu)體而 Vec<T> 是結(jié)構(gòu)體總位于索引 0 的項。接著就可以使用 Wrapper 中 Display 的功能了。

此方法的缺點是,因為 Wrapper 是一個新類型,它沒有定義于其值之上的方法;必須直接在 Wrapper 上實現(xiàn) Vec<T> 的所有方法,這樣就可以代理到self.0 上 —— 這就允許我們完全像 Vec<T> 那樣對待 Wrapper。如果希望新類型擁有其內(nèi)部類型的每一個方法,為封裝類型實現(xiàn) Deref trait(第十五章 “通過 Deref trait 將智能指針當作常規(guī)引用處理” 部分討論過)并返回其內(nèi)部類型是一種解決方案。如果不希望封裝類型擁有所有內(nèi)部類型的方法 —— 比如為了限制封裝類型的行為 —— 則必須只自行實現(xiàn)所需的方法。

上面便是 newtype 模式如何與 trait 結(jié)合使用的;還有一個不涉及 trait 的實用模式?,F(xiàn)在讓我們將話題的焦點轉(zhuǎn)移到一些與 Rust 類型系統(tǒng)交互的高級方法上來吧。


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

掃描二維碼

下載編程獅App

公眾號
微信公眾號

編程獅公眾號