建立於 2023-06-30 更新於 2026-07-06
# rust

Trait

Trait 可以為不同的類型定義一個共享的行為。

trait 有點類似其他語言的 interface,但有些不同。

定義 Trait

一個類型由其可調用的方法組成。如果可以對不同類型調用相同的方法,這些類型就可以共享相同的行為了。

Trait 定義是一種將方法簽名組合起來的方法,目的是定義一個實現某些目的所必需的行為的集合。

假設我們有多個存放不同類型資料的結構 NewsArticle 與 Tweet, NewsArticle 用來儲存世界各地的新聞,Tweet 用來儲存最多 280 字的推文。 我們希望為這兩個結構實現一個 summarize 方法,以便在需要時顯示摘要。

trait Summary {
    // 只定義了一個方法簽名,但沒有實際的方法內容
    fn summarize(&self) -> String;
}

為類型實現 Trait

為類型實現 Trait 的語法如下:

pub trait Summary {
    fn summarize(&self) -> String;
}

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

一旦實作了 trait,在結構定義好之後就可以直接使用。

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

如果別人想使用我們定義的 Summary trait,要先引入我的們 trait, 這也是為什麼 trait 前面會有一個 pub 的原因。

// 使用 pub 讓其他模組可以引入
pub trait Summary {
    fn summarize(&self) -> String;
}

引入 trait 的語法如下:

use learning_rust::Summary;

需要注意的是,只有當 trait 或結構定義在同一個模組中時,才可以直接使用。 例如可以為自定義 Tweet 實作標準庫中的 Display trait,這是因為 Tweet 位於多媒體聚合庫 crate 的本地作用域中。 你也可以在多媒體聚合庫 crate 中為 Vec 實作 Summary trait,因為 Summary 位於多媒體聚合庫 crate 的本地作用域中。

但不能為外部類型實現外部 trait,例如不能在多媒體聚合庫中為 Vec 實作 Display trait,因為 Display 和 Vec 都位於標準庫中,而不是多媒體聚合庫的本地作用域中。

這個限制被稱為相干性 (coherence) 或孤立性 (orphan rule),其主要目的是避免衝突,因為如果允許外部實作外部 trait,就可能會有多個相同的實作,而編譯器無法判斷要使用哪一個。

默認實作

Trait 可以提供默認實作,這樣實作該 trait 的類型就不需要實作該方法了。

pub trait Summary {
    fn summarize(&self) -> String {
        String::from("(Read more...)")
    }
}

如果實作了默認實作,那麼實作該 trait 的類型就可以直接使用該方法了。

impl Summary for Tweet {}

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

Trait 可以有多個方法,也可以部分有默認實作,部分沒有默認實作。

pub trait Summary {
    fn summarize_author(&self) -> String;

    fn summarize(&self) -> String {
        format!("(Read more from {}...)", self.summarize_author())
    }
}

如果想要使用這個版本的 Summary trait,只需要實作 summarize_author 方法即可。

impl Summary for Tweet {
    fn summarize_author(&self) -> String {
        format!("@{}", self.username)
    }
}

用 Trait 來限制參數類型

Trait 可以用來限制函數參數的類型。

// 在 notify 函數中,item 參數的類型是實作了 Summary trait 的類型
pub fn notify(item: impl Summary) {
    println!("Breaking news! {}", item.summarize());
}

Trait Bound 語法

pub fn notify<T: Summary>(item: T) {
    println!("Breaking news! {}", item.summarize());
}

較為複雜的場景,可以考慮使用 trait bound 語法,例如多個參數

pub fn notify(item1: impl Summary, item2: impl Summary) {}

可以改寫成

pub fn notify<T: Summary>(item1: T, item2: T) {}

通過 + 指定多個 Trait

pub fn notify(item: impl Summary + Display) {}

這個寫法的意思是,item 參數必須實作 Summary 和 Display 這兩個 trait。

Trait bound 語法也可以這樣寫:

pub fn notify<T: Summary + Display>(item: T) {}

通過 where 簡化 Trait Bound

使用過多的 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
{}

返回實作了 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,
    }
}

使用 Trait Bound 有條件的實現方法

通過 trait bound,可以有條件的只為實作了某個 trait 的類型實現方法。

use std::fmt::Display;

struct Pair {
    x: T,
    y: T,
}

impl Pair {
    fn new(x: T, y: T) -> Self {
        Self {
            x,
            y,
        }
    }
}

// 只為實作了 Display 和 PartialOrd trait 的類型實現 cmp_display 方法
impl<T: Display + PartialOrd> Pair {
    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);
        }
    }
}

也可以對任何實現特定 trait 的類型有條件的實作 trait。例如標準庫為任何實現了 Display trait 的類型實作了 ToString trait。

這也被稱為 blanket implementations

impl<T: Display> ToString for T {
    // --snip--
}