Rust trait generic polymorphism dyn

[Rust] Trait

Rust 系列最後一篇。前面把資料(型別、struct、enum、集合)跟組織(模組、可見性)都鋪好了,這一篇補上最後一塊——「行為」的抽象。Trait 是 Rust 的多型機制,一個人兼任 interface、type class 與泛型約束三種角色;DisplayDebug 這類常用 trait 也會在這篇收尾。

Trait 是什麼

Trait 定義「一組型別共同擁有的行為」,類似其他語言的 interface / protocol。差別在於 Rust 的 trait 還能:

  • 提供預設方法實作
  • 作為泛型約束(trait bound)
  • 透過 dyn Trait 做動態派發
  • 透過 impl Trait 簡化型別

基本定義與實作

trait Greet {
    fn say_hi(&self) -> String;
}

struct English;
struct Japanese;

impl Greet for English {
    fn say_hi(&self) -> String {
        String::from("Hello")
    }
}

impl Greet for Japanese {
    fn say_hi(&self) -> String {
        String::from("こんにちは")
    }
}

fn main() {
    println!("{}", English.say_hi());
    println!("{}", Japanese.say_hi());
}

預設方法

trait 可以給方法寫預設實作,實作方可以直接用、也可以覆寫:

trait Animal {
    fn name(&self) -> String;

    // 有預設實作
    fn greet(&self) -> String {
        format!("Hi, I'm {}", self.name())
    }
}

struct Dog;
impl Animal for Dog {
    fn name(&self) -> String {
        String::from("Rex")
    }
    // greet 直接用預設
}

struct Cat;
impl Animal for Cat {
    fn name(&self) -> String { String::from("Whiskers") }
    fn greet(&self) -> String {
        String::from("Meow, get out of my way")
    }
}

預設實作可以呼叫 trait 內的其他方法,不管那個方法有沒有預設。

Trait bound:把 trait 當泛型約束

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

// 寫法 1:trait bound
fn notify<T: Summary>(item: &T) {
    println!("Breaking news! {}", item.summarize());
}

// 寫法 2:impl Trait(語法糖,幾乎等價)
fn notify_v2(item: &impl Summary) {
    println!("Breaking news! {}", item.summarize());
}

// 多重 bound
fn notify_v3<T: Summary + std::fmt::Display>(item: &T) {
    println!("Display: {item}, Summary: {}", item.summarize());
}

// where 子句:bound 多時更好讀
fn complex<T, U>(t: &T, u: &U) -> i32
where
    T: Summary + Clone,
    U: std::fmt::Debug + Clone,
{
    0
}

<T: Summary>impl Summary 的差異:

  • <T: Summary>:呼叫方可以指定型別 notify::<Article>(&a)
  • impl Summary:不能在呼叫端指定型別,較不靈活但語法乾淨

另一個差別:兩個參數都用 impl 跟都用 T 不一樣——

// 兩個參數可以不同型別(只要都實作 Summary)
fn f(a: &impl Summary, b: &impl Summary) {}

// 兩個參數必須同型別
fn g<T: Summary>(a: &T, b: &T) {}

回傳 impl Trait

回傳值的具體型別寫起來太麻煩時(特別是 closure、iterator),用 impl Trait 省事:

fn make_adder(x: i32) -> impl Fn(i32) -> i32 {
    move |y| x + y
}

fn evens(v: Vec<i32>) -> impl Iterator<Item = i32> {
    v.into_iter().filter(|n| n % 2 == 0)
}

限制:函式只能回傳「同一個」具體型別。下面這個會失敗:

fn pick(flag: bool) -> impl Greet {
    if flag {
        English          // ❌ 兩個分支型別不同
    } else {
        Japanese
    }
}

要處理這種「執行期才知道是哪個型別」的情況,就要靠 trait object。

Trait object:dyn Trait(動態派發)

dyn Trait 表示「某個實作了 Trait 的型別,但具體是哪個要執行期才知道」。它必須包在指標後面(&dynBox<dyn>Rc<dyn>…),因為大小在編譯期未知。

為什麼一定要指標?因為 trait object 的具體型別到執行期才確定——可能是 5 byte 的某個 struct,也可能是 200 byte 的另一個——編譯期不知道 stack 上要預留多大空間。包成指標之後,指標本身大小固定,編譯期就有答案了。

fn pick(flag: bool) -> Box<dyn Greet> {
    if flag {
        Box::new(English)
    } else {
        Box::new(Japanese)
    }
}

fn main() {
    let g = pick(true);
    println!("{}", g.say_hi());
}

異質集合:

let greeters: Vec<Box<dyn Greet>> = vec![
    Box::new(English),
    Box::new(Japanese),
];

for g in &greeters {
    println!("{}", g.say_hi());
}

怎麼選:型別在編譯期就知道,用泛型;要在執行期才決定具體型別、或要把不同型別放進同一個容器,用 trait object。

泛型 + trait boundtrait object(dyn)
派發方式靜態(單形化 monomorphization)動態(vtable)
效能接近手寫,無 indirection多一次間接呼叫
二進位大小較大(每個型別產生一份程式碼)較小
異質集合

Object safety

不是所有 trait 都能做成 trait object。trait 必須是 object safe 才能用 dyn。常見限制:

  • 方法不能有泛型參數
  • 方法不能回傳 Self
  • 不能有 associated function(沒 &self

原因出在 vtable(函式指標表):動態派發靠它找方法,每個欄位只能放一個函式指標。泛型方法會依 T 實體化成多個版本,一張 vtable 放不下;回傳 Self 也不行,因為不同實作的 Self 大小不同,caller 無法預知 stack 佈局。

trait Bad {
    fn make() -> Self;            // ❌ 用不了 dyn Bad
}

trait Good {
    fn name(&self) -> String;     // ✅
}

違反時編譯器訊息會明確指出原因。

常用標準 trait

幾個會反覆遇到的:

Debug、Display

Debug 用在開發時({:?}),Display 是給使用者看的({})。

use std::fmt;

#[derive(Debug)]                       // 自動實作 Debug
struct Point { x: i32, y: i32 }

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

fn main() {
    let p = Point { x: 1, y: 2 };
    println!("{p:?}");      // Point { x: 1, y: 2 }
    println!("{p}");        // (1, 2)
}

Clone、Copy

#[derive(Clone, Copy)]
struct Coord { x: i32, y: i32 }

let a = Coord { x: 1, y: 2 };
let b = a;          // Copy:a 還能用
let c = a.clone();

CopyClone 的子 trait,只能用在「按位元複製就夠」的型別——含 StringVec 等的就不行。

PartialEq、Eq

支援 ==!= 比較。

#[derive(PartialEq, Eq)]
struct Id(u32);

let a = Id(1);
let b = Id(1);
assert!(a == b);

Eq 表示「自反、對稱、傳遞」都成立。f64 就不算,因為 NaN != NaN

PartialOrd、Ord

支援大小比較(<>.cmp())。

#[derive(PartialEq, Eq, PartialOrd, Ord)]
struct Score(i32);

Default

提供預設值。

#[derive(Default)]
struct Config {
    debug: bool,        // false
    retries: u32,       // 0
    name: String,       // ""
}

let c = Config::default();
let c = Config { retries: 3, ..Default::default() };

From、Into

型別轉換。慣例只實作 FromInto 自動就有。

struct Celsius(f64);
struct Fahrenheit(f64);

impl From<Celsius> for Fahrenheit {
    fn from(c: Celsius) -> Self {
        Fahrenheit(c.0 * 9.0 / 5.0 + 32.0)
    }
}

let f: Fahrenheit = Celsius(100.0).into();
let f = Fahrenheit::from(Celsius(100.0));

Iterator

只要實作 next,就免費獲得 .map.filter.collect 等一大串方法。

struct Counter { n: u32 }

impl Iterator for Counter {
    type Item = u32;
    fn next(&mut self) -> Option<u32> {
        if self.n < 5 {
            self.n += 1;
            Some(self.n)
        } else {
            None
        }
    }
}

fn main() {
    let sum: u32 = Counter { n: 0 }.sum();   // 1+2+3+4+5 = 15
}

type Item = u32;associated type:trait 內定義一個型別槽,由實作方填上具體型別。

associated type 每個 impl 只能填一個值——Iterator for MyIter 只能有一種 Item。如果改成泛型 Iterator<T>,同一個型別就可以對不同 T 各實作一次,更彈性,但 API 也更囉嗦。Rust 選 associated type,因為大多數情況只需要一種對應關係。

孤兒規則(orphan rule)

要寫 impl Trait for TypeTrait 或 Type 至少有一個必須定義在你自己的 crate 內

// ✅ 自家 trait 給標準型別
impl MyTrait for Vec<i32> {}

// ✅ 標準 trait 給自家型別
impl std::fmt::Display for MyType {}

// ❌ 標準 trait 給標準型別
impl std::fmt::Display for Vec<i32> {}

這個規則防止兩個 crate 同時對 Vec<i32> 實作 Display 而起衝突。

要繞過就用 newtype 包一層:

struct Wrapper(Vec<i32>);

impl std::fmt::Display for Wrapper {
    fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
        write!(f, "{:?}", self.0)
    }
}

一個整合範例:用 trait 抽象資料來源

trait DataSource {
    fn fetch(&self, id: u32) -> Option<String>;
    fn name(&self) -> &str;

    // 預設實作
    fn fetch_all(&self, ids: &[u32]) -> Vec<String> {
        ids.iter().filter_map(|&id| self.fetch(id)).collect()
    }
}

struct InMemory {
    data: std::collections::HashMap<u32, String>,
}

impl DataSource for InMemory {
    fn fetch(&self, id: u32) -> Option<String> {
        self.data.get(&id).cloned()
    }
    fn name(&self) -> &str { "memory" }
}

struct Stub;

impl DataSource for Stub {
    fn fetch(&self, _id: u32) -> Option<String> {
        Some(String::from("stub"))
    }
    fn name(&self) -> &str { "stub" }
}

// 泛型版(單形化、靜態派發)
fn print_one<T: DataSource>(src: &T, id: u32) {
    if let Some(v) = src.fetch(id) {
        println!("[{}] {v}", src.name());
    }
}

// trait object 版(動態派發)
fn print_all(sources: &[Box<dyn DataSource>], id: u32) {
    for s in sources {
        if let Some(v) = s.fetch(id) {
            println!("[{}] {v}", s.name());
        }
    }
}

Trait 思考順序

設計 API 時可以照這個順序判斷:

  1. 不需要多型 → 直接寫具體型別
  2. 編譯期能確定型別 → 泛型 + trait bound(fn f<T: Trait> / impl Trait
  3. 執行期才確定 / 異質集合 → trait object(Box<dyn Trait>
  4. trait 設計上會擋掉 dyn(回傳 Self、泛型方法等)→ 要嘛拆 trait,要嘛只用泛型

Rust 系列到此告一段落。八篇下來:環境、變數型別、所有權、結構與列舉、錯誤處理、集合、模組、trait——剛好構成寫真實 Rust 程式所需的最小完整地圖。剩下的(async、巨集、unsafe、各 ecosystem 框架)是分支,回頭再展開。

Latest Updates

  • 2026.06.11 Content updated