天天看點

Rust學習筆記(五十六)進階trait

作者:凡事不平凡coding

在trait中使用關聯類型來指定占位類型

關聯類型(associated type)是trait中的類型占位符,它可以用于trait的方法簽名中:可以定義出包含某些類型的trait,而在實作前無需知道這些類型是什麼。例:

pub trait Iterator {
    type Item;//關聯類型
    fn next(&self) -> Option<Self::Item>;
}
           

關聯類型與泛型的差別

泛型:

  • 每次實作trait時必須标注類型
  • 可以為一個類型多次實作某個trait(不同的泛型參數),例如為Counter的String和u32分别實作Iterator
pub trait Iterator<T> {
    fn next(&mut self) -> Option<T>;
}
struct Counter {}
impl Iterator<String> for Counter {
    fn next(&mut self) -> Option<String> {
        todo!()
    }
}
impl Iterator<u32> for Counter {
    fn next(&mut self) -> Option<u32> {
        todo!()
    }
}
           

關聯類型:

  • 實作時無需标注類型
  • 無法為單個類型多次實作某個trait

關聯類型的實作:

pub trait Iterator {
    type Item;
    fn next(&mut self) -> Option<Self::Item>;
}

struct Counter {}

impl Iterator for Counter {
    //無需标注類型

    type Item = u32;

    fn next(&mut self) -> Option<Self::Item> {
        todo!()
    }
}
           

預設泛型參數和運算符重載

可以在使用泛型參數時為泛型指定一個預設的具體類型,文法:<PlaceholderType=ConcreteType> 這種技術常用于運算符重載(operator overloading)。雖然Rust不允許建立自己的運算符以及重載任意的運算符,但是可以通過實作std::ops中列出的那些trait來重載一部分相應的運算符。例:

use std::ops::Add;

#[derive(Debug, 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 }
    );
}
           

以上代碼實作 Add trait 以重載 Point 執行個體的 + 運算符。 Add trait源碼如下:

trait Add<RHS=Self> {
    type Output;

    fn add(self, rhs: RHS) -> Self::Output;
}
           
這看來應該很熟悉,這是一個帶有一個方法和一個關聯類型的 trait。比較陌生的部分是尖括号中的 RHS=Self:這個文法叫做 預設類型參數(default type parameters)。RHS 是一個泛型類型參數(“right hand side” 的縮寫),它用于定義 add 方法中的 rhs 參數。如果實作 Add trait 時不指定 RHS 的具體類型,RHS 的類型将是預設的 Self 類型,也就是在其上實作 Add 的類型。
當為 Point 實作 Add 時,使用了預設的 RHS,因為我們希望将兩個 Point 執行個體相加。讓我們看看一個實作 Add trait 時希望自定義 RHS 類型而不是使用預設類型的例子。
這裡有兩個存放不同單元值的結構體,Millimeters 和 Meters。我們希望能夠将毫米值與米值相加,并讓 Add 的實作正确處理轉換。可以為 Millimeters 實作 Add 并以 Meters 作為 RHS,例:
use std::ops::Add;

#[derive(Debug)]
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))
    }
}

fn main() {
    println!("{:?}", Millimeters(1000) + Meters(1));
}
           

預設泛型參數類型的主要應用場景

  • 擴充一個類型而不破壞現有代碼
  • 允許在大部分使用者都不需要的特定場景下進行自定義
标準庫的 Add trait 就是一個第二個目的例子:大部分時候你會将兩個相似的類型相加,不過它提供了自定義額外行為的能力。在 Add trait 定義中使用預設類型參數意味着大部分時候無需指定額外的參數。換句話說,一小部分實作的樣闆代碼是不必要的,這樣使用 trait 就更容易了。
第一個目的是相似的,但過程是反過來的:如果需要為現有 trait 增加類型參數,為其提供一個預設類型将允許我們在不破壞現有實作代碼的基礎上擴充 trait 的功能。

完全限定文法(Fully Qualifiled Syntax)如何調用同名方法

trait Pilot {
    fn fly(&self);
}

trait Wizard {
    fn fly(&self);
}

struct Human;

impl Pilot for Human {//實作Pilot的fly方法
    fn fly(&self) {
        println!("This is your captain speaking.");
    }
}

impl Wizard for Human {//實作Wizard的fly方法
    fn fly(&self) {
        println!("Up!");
    }
}

impl Human {//實作自己的fly方法
    fn fly(&self) {
        println!("*waving arms furiously*");
    }
}

fn main() {
    let person = Human;
    person.fly();//調用本身的fly方法
    Pilot::fly(&person);//調用Pilot的fly方法
    Wizard::fly(&person);//調用Wizard的fly方法
}
           

Pilot和Wizard除了Person實作外,還可能有其它類型的實作,那麼是怎麼分辨出是Person的實作呢?因為fly方法有self參數,我們将&person傳進去後,就知道是person的類型實作了。如果是沒有參數的關聯函數呢?

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

對于上面的例子我們怎麼調用Dog類型實作的Animal trait裡的關聯函數呢?那就可以用到完全限定文法了。

<Type as Trait>::function(receiver_if_method, next_arg, ...);
           

它可以在任何調用函數或方法的地方使用,允許忽略那些從其它上下文能推導出來的部分。隻有當Rust無法區分我們期望調用哪個具體實作的時候,才需要使用這種文法。

fn main() {
    println!("A baby dog is called a {}", Dog::baby_name());
    println!("A baby dog is called a {}", <Dog as Animal>::baby_name());
}
           

使用supertrait來要求trait附帶其它trait的功能

有時需要在一個trait中使用其它trait的功能:

  • 需要被依賴的trait也被實作
  • 那個被間接依賴的trait也就是目前trait的supertrait

例如實作OutlinePrint trait的同時還要求實作fmt::Display trait

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

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

impl OutlinePrint for Point {}

impl fmt::Display for Point {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        write!(f, "({}, {})", self.x, self.y)
    }
}
           

使用newtype模式在外部類型上實作外部trait

我們之前學習過孤兒規則:隻有當trait或類型定義在本地包時,才能為該類型實作這個trait。但是我們可以通過newtype模式繞過這一規則。也就是利用tuple struct(元組結構體)建立一個新的類型,例:

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

以上例子為Vec<String>實作fmt::Display trait,但是它們都不在我們我們的crate内,是以定義一個新的元組結構體Wrapper,讓它包裹着Vec<String>,這樣就可以間接的為Vec<String>實作fmt::Display trait了。

繼續閱讀