這個章節會詳細介紹特性的關聯型別(Associated Type)、運算子多載(Operator Overloading)的實作方式、呼叫名稱相同但屬於不同特性的方法或關聯函數、替外部的型別實作新的特性。



特性的關聯型別

我們在先前的章節使用Iterator特性實作迭代器的時候,就有用到特性的關聯型別,但是並沒有針對它進行說明。特性的關聯型別就是定義出一個名稱來代表一個型別,且這個名稱可以被使用在特性內所定義的關聯函數或是方法。也就是說,當我們使用impl關鍵字來實作這個特性時,可以決定特性的關聯型別名稱所代表的實際型別,能使同一個特性在不同的實作下,其關聯函數和方法能夠擁有不同的簽名和回傳值型別。

舉例來說:

trait MyTrait {
    type SomeType;

    fn do_something(&mut self, obj: Self::SomeType);
}

struct MyStruct1 {}

impl MyTrait for MyStruct1 {
    type SomeType = i32;

    fn do_something(&mut self, obj: Self::SomeType) {
        println!("{obj}");
    }
}

struct MyStruct2 {}

impl MyTrait for MyStruct2 {
    type SomeType = String;

    fn do_something(&mut self, obj: Self::SomeType) {
        println!("{obj}");
    }
}

fn main() {}

以上程式,MyTrait特性有一個關聯型別名稱SomeType,且do_something方法的obj參數的型別為SomeType。程式第9行到第15行,替MyStruct1結構體實作MyTrait特性,將關聯型別名稱SomeType指定為i32型別,因此do_something方法的obj參數的型別為i32。程式第19行到第25行,替MyStruct2結構體實作MyTrait特性,將關聯型別名稱SomeType指定為String型別,因此do_something方法的obj參數的型別為String

當然,我們之前學到的泛型也可以完成類似的功能。舉例來說:

trait MyTrait<SomeType> {
    fn do_something(&mut self, obj: SomeType);
}
 
struct MyStruct1 {}
 
impl MyTrait<i32> for MyStruct1 {
    fn do_something(&mut self, obj: i32) {
        println!("{obj}");
    }
}
 
struct MyStruct2 {}
 
impl MyTrait<String> for MyStruct2 {
    fn do_something(&mut self, obj: String) {
        println!("{obj}");
    }
}
 
fn main() {}

使用泛型還有一個好處,就是我們替相同的型別實作很多種MyTrait特性。舉例來說:

trait MyTrait<SomeType> {
    fn do_something(&mut self, obj: SomeType);
}

struct MyStruct1 {}

impl MyTrait<i32> for MyStruct1 {
    fn do_something(&mut self, obj: i32) {
        println!("{obj}");
    }
}

impl MyTrait<String> for MyStruct1 {
    fn do_something(&mut self, obj: String) {
        println!("{obj}");
    }
}

fn main() {}

如果要把擁有關聯型別的特性作為型別來使用的話,無法直接使用特性名稱來代替型別名稱。舉例來說:

trait MyTrait {
    type SomeType;

    fn do_something(&mut self, obj: Self::SomeType);
}

struct MyStruct {}

impl MyTrait for MyStruct {
    type SomeType = i32;

    fn do_something(&mut self, obj: Self::SomeType) {
        println!("{}", obj);
    }
}

struct HoldMyTrait {
    data: Box<dyn MyTrait>
}

fn main() {}

以上程式第18行會編譯失敗,因為MyTrait特性有使用到關聯型別,我們必須要替MyTrait特性的關聯型別對應的型別明確定義出來,才可以將MyTrait特性作為型別使用。這部份其實就類似泛型,寫法如下:

trait MyTrait {
    type SomeType;

    fn do_something(&mut self, obj: Self::SomeType);
}

struct MyStruct {}

impl MyTrait for MyStruct {
    type SomeType = i32;

    fn do_something(&mut self, obj: Self::SomeType) {
        println!("{obj}");
    }
}

struct HoldMyTrait {
    data: Box<dyn MyTrait>,
}

fn main() {}

利用=符號,可以在<>泛型語法中指定關聯型別名稱所對應的型別。

那如果特性同時有關聯型別和泛型的話該怎麼辦呢?舉例來說:

trait MyTrait<T> {
    type SomeType;

    fn do_something(&mut self, obj: Self::SomeType);
}

struct MyStruct {}

impl<T> MyTrait<T> for MyStruct {
    type SomeType = i32;

    fn do_something(&mut self, obj: Self::SomeType) {
        println!("{obj}");
    }
}

struct HoldMyTrait {
    data: Box<dyn MyTrait<i32, SomeType=MyStruct>>
}

我們必須將<>泛型語法中的關聯型別對應放在泛型型別參數後面。

運算子多載

我們無法在Rust程式語言中建立出新的運算子,也無法多載(overload)任意運算子。我們只能透過實作標準函式庫中的std::ops模組所提供的相關特性,來完成運算子的多載。例如我們想要讓我們實作的結構實體能支援+運算,我們就必須替該結構體實作Add特性。

我們可以觀察一下Add特性的定義:

trait Add<RHS=Self> {
    /// The resulting type after applying the `+` operator.
    #[stable(feature = "rust1", since = "1.0.0")]
    type Output;

    /// Performs the `+` operation.
    #[stable(feature = "rust1", since = "1.0.0")]
    fn add(self, rhs: RHS) -> Self::Output;
}

Add特性有一個泛型型別參數RHS,這邊有個我們之前沒學過的語法,那就是利用等號=來設定泛型型別參數的「預設值」。舉例來說:

trait MyTrait<T = i32> {
    fn do_something(&mut self, obj: T);
}

struct MyStruct1 {}

impl MyTrait for MyStruct1 {
    fn do_something(&mut self, obj: i32) {
        println!("{obj}");
    }
}

impl MyTrait<String> for MyStruct1 {
    fn do_something(&mut self, obj: String) {
        println!("{obj}");
    }
}

fn main() {}

以上程式第7行,MyTrait其實就是MyTrait<i32>

因此,如果不特別指定Add特性的泛型型別參數RHS究竟是哪個型別的話,預設就會是實作這個特性的型別自己。舉例來說:

use std::ops::Add;

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

impl Add<(i32, i32)> for Point {
    type Output = Point;

    fn add(self, other: (i32, i32)) -> Point {
        Point { x: self.x + other.0, y: self.y + other.1 }
    }
}

fn main() {
    println!("{:?}", Point { x: 1, y: 0 } + Point { x: 2, y: 3 });
    println!("{:?}", Point { x: 1, y: 0 } + (2, 3));
}

以上程式,第9行的Add其實就是Add<Point>。我們替Point結構體實作了Add<Point>Add<(i32, i32)>,因此Point結構實體可以和Point結構實體或是(i32, i32)元組進行+運算。程式執行結果如下:

Point { x: 3, y: 3 }
Point { x: 3, y: 3 }

呼叫名稱相同但屬於不同特性的方法

先看看以下程式碼吧!

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

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

    person.fly();
}

這個程式第26行會編譯錯誤,因為Human結構體實作的Pilot特性和Wizard特性都有fly方法,編譯器不知道到底要執行哪個fly方法。不過,如果Human結構體本身也有實作fly方法的話,程式第26行是可以通過編譯的,且只會呼叫到Human結構體本身實作的fly方法。如下:

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

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

    person.fly();
}

我們雖然替Human結構體本身實作了fly方法,但如果我們還是想要使用到Pilot特性和Wizard特性的fly方法時該怎麼辦呢?其實很簡單,只要換個語法來呼叫方法就好,程式如下:

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

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

    Pilot::fly(&person);
    Wizard::fly(&person);
    person.fly();
}

呼叫名稱相同但屬於不同特性的關聯函數

剛才我們介紹的是呼叫名稱相同但屬於不同特性的「方法」,現在來看看如果是要呼叫「關聯函數」的話,要怎麼做吧!

先看看以下程式碼:

trait Pilot {
    fn fly();
}

trait Wizard {
    fn fly();
}

struct Human {}

impl Pilot for Human {
    fn fly() {
        println!("This is your captain speaking.");
    }
}

impl Wizard for Human {
    fn fly() {
        println!("Up!");
    }
}

fn main() {
    Human::fly();
}

這個程式第24行會編譯錯誤,因為Human結構體實作的Pilot特性和Wizard特性都有fly關聯函數,編譯器不知道到底要執行哪個fly關聯函數。不過,如果Human結構體本身也有實作fly關聯函數的話,程式第24行是可以通過編譯的,且只會呼叫到Human結構體本身實作的fly關聯函數。如下:

trait Pilot {
    fn fly();
}

trait Wizard {
    fn fly();
}

struct Human {}

impl Pilot for Human {
    fn fly() {
        println!("This is your captain speaking.");
    }
}

impl Wizard for Human {
    fn fly() {
        println!("Up!");
    }
}

impl Human {
    fn fly() {
        println!("*waving arms furiously*");
    }
}

fn main() {
    Human::fly();
}

我們雖然替Human結構體本身實作了fly關聯函數,但如果我們還是想要使用到Pilot特性和Wizard特性的fly關聯函數時該怎麼辦呢?關聯函數並不像方法一樣能夠接受self參數,有辦法用類似剛才介紹呼叫方法的方式來呼叫關聯函數嗎?我們嘗試撰寫出以下程式:

trait Pilot {
    fn fly();
}

trait Wizard {
    fn fly();
}

struct Human {}

impl Pilot for Human {
    fn fly() {
        println!("This is your captain speaking.");
    }
}

impl Wizard for Human {
    fn fly() {
        println!("Up!");
    }
}

impl Human {
    fn fly() {
        println!("*waving arms furiously*");
    }
}

fn main() {
    Pilot::fly();
    Wizard::fly();
    Human::fly();
}

程式第30行和第31行會編譯錯誤,因為編譯器不知道特性的抽象關聯函數究竟是如何實作的,我們必須要使用<Type as Trait>::function這樣的語法來指定要呼叫型別的哪個特性所實作的關聯函數。程式需要改寫如下:

trait Pilot {
    fn fly();
}

trait Wizard {
    fn fly();
}

struct Human {}

impl Pilot for Human {
    fn fly() {
        println!("This is your captain speaking.");
    }
}

impl Wizard for Human {
    fn fly() {
        println!("Up!");
    }
}

impl Human {
    fn fly() {
        println!("*waving arms furiously*");
    }
}

fn main() {
    <Human as Pilot>::fly();
    <Human as Wizard>::fly();
    Human::fly();
}

替外部的型別實作新的特性

替型別實作特性,該型別和特性兩者中必須要有一個是定義在目前的crate,否則無法編譯。

舉例來說:

use std::fmt::Display;
use std::fmt::Formatter;
use std::fmt::Error;

impl Display for Vec<i32> {
    fn fmt(&self, f: &mut Formatter) -> Result<(), Error> {
        Ok(())
    }
}

fn main() {}

以上程式,嘗試替Rust程式語言標準函式庫中的Vec<i32>結構體實作同樣位於標準函式庫中的Display特性,由於Vec結構體和Display特性沒有一個是定義在目前的crate下,因此程式會編譯失敗。想要讓這個程式能夠編譯成功,我們需要在目前的crate建立一個新的結構體來儲存Vec結構實體,有點類似替一個既有的型別建立出一個新的型別的概念,且為了簡化結構體,通常會使用元組結構體來完成這件事。程式如下:

use std::fmt::Display;
use std::fmt::Formatter;
use std::fmt::Error;

struct MyVec<T>(Vec<T>);

impl Display for MyVec<i32> {
    fn fmt(&self, f: &mut Formatter) -> Result<(), Error> {
        Ok(())
    }
}

fn main() {}

建立出我們自己新型別除了可以讓我們能夠替它實作更多來自於其它crate的特性之外,還可以重新替原本的型別定義新的公開的關聯函數或是方法。我們也可以將原本擁有泛型參數的型別包裝成沒有泛型參數的型別,使它更容易被使用。

結論

這個章節我們學會了特性的關聯型別用法,也知道如何實作標準函式庫中的std::ops模組提供的相關特性來多載運算子,能夠讓我們實作出來的結構體或是列舉更方便使用。我們甚至還考慮到當遇到特性的關聯函數和方法名稱有衝突時的情況,能夠使用不同的語法來將它們區分出來呼叫。也學會了利用建立一個新結構體來包裹一個不在目前crate中的型別的方式,來實作也不在目前crate中的特性。

在下一章節,我們將會學習進階的型別用法。

下一章:進階的型別用法