首页 / Rust 入门教程 / 默认实现与 trait 约束

Rust 入门教程

默认实现与 trait 约束

本教程共 78 篇 · 第 45 篇 · 更新于 2026-08-08 · 约 10 分钟阅读

RustRust 入门教程trait默认实现trait boundimpl Traitwhere约束

本节目标:学会给 trait 方法写默认实现,搞清 impl Trait 与 trait bound 的等价关系,并掌握多重约束、where 子句和条件实现。

上一章讲了 trait 的定义与实现。这一章补两块关键内容:默认实现,以及如何把 trait 当作约束来精确控制泛型。

1-1 trait 的默认实现

trait 里的方法,可以不只写签名,还能直接给默认实现。实现该 trait 的类型,不写就用默认的,想改就重写:

pub trait Summary {
    fn summarize(&self) -> String {
        String::from("(阅读更多...)")
    }
}

impl Summary for Post {} // 用默认实现

impl Summary for Weibo {
    fn summarize(&self) -> String {
        format!("{}发表了微博{}", self.username, self.content)
    }
}

Post 后面是空花括号,表示”沿用默认”;Weibo 重写了方法。调用时各取所需。

更妙的是,默认实现能调用同一个 trait 里其他还没默认实现的方法。这样使用者只需实现一小部分,就能白嫖一大堆功能:

pub trait Summary {
    fn summarize_author(&self) -> String; // 必须自己实现

    fn summarize(&self) -> String {
        format!("(更多来自 {}...)", self.summarize_author())
    }
}

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

这里只实现了 summarize_authorsummarize 的默认实现会自动调用它。这种”留一个钩子、其余打包”的模式,在标准库里到处可见。

1-2 impl Trait 与 trait bound 等价

上一章的 &impl Summary 其实只是语法糖。它的完整写法是 trait bound(trait 约束)

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

T: Summary 读作”T 实现了 Summary”。简单场景用 impl Trait 更清爽;但复杂场景只有 trait bound 能表达。

比如要强制两个参数是同一类型,只能靠 trait bound:

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

notify(item1: &impl Summary, item2: &impl Summary) 允许两个参数类型不同,约束不住”同类型”。

把两种写法摆一起看更直观:

// 写法 A:两个参数可以是不同类型,比如 Post + Weibo 混着传
fn notify_a(item1: &impl Summary, item2: &impl Summary) {}

// 写法 B:两个参数必须是同一个类型 T
fn notify_b<T: Summary>(item1: &T, item2: &T) {}

写法 A 里,item1Postitem2Weibo 完全合法;写法 B 里,两个都得是 Post(或都得是 Weibo),编译器会确保它们是同一类型。所以”要不要强制同类型”是选语法糖还是 trait bound 的关键分水岭。

1-3 多重约束

一个类型可能需要同时满足多个 trait。写法是用 + 连接:

pub fn notify(item: &(impl Summary + Display)) {}
// 等价写法:
pub fn notify<T: Summary + Display>(item: &T) {}

这样函数体内既能调 item.summarize(),也能用 println!("{}", item) 做格式化输出。

1-4 where 子句救场

约束一多,函数签名就变长变丑:

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,
{
    // ...
}

泛型参数、参数、返回值集中在一行,约束单独一行,读起来更快。约束多时强烈建议用 where

1-5 条件实现:约束决定方法存不存在

trait 约束还能用来做条件实现——只有当类型满足某些约束,才拥有某个方法:

use std::fmt::Display;

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

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

impl<T: Display + PartialOrd> Pair<T> {
    fn cmp_display(&self) {
        if self.x >= self.y {
            println!("较大的是 x = {}", self.x);
        } else {
            println!("较大的是 y = {}", self.y);
        }
    }
}

cmp_display 不是所有 Pair<T> 都有,只有 T 同时实现了 DisplayPartialOrdPair 才有。因为方法体里要打印、要比较,缺了哪个 trait 都编不过,约束正好表达了这个前提。

也可以用约束做”条件实现 trait”。标准库的经典例子:

impl<T: Display> ToString for T {
    // ...
}

意思是”任何实现了 Display 的类型,都自动获得 ToString”。所以 3.to_string() 能成立,因为整数实现了 Display

1-6 用 impl Trait 作返回值

函数还能返回”一个实现了某 trait 的类型”,用 impl Trait 当返回类型:

fn returns_summarizable() -> impl Summary {
    Weibo {
        username: String::from("max"),
        content: String::from("今天学 Rust"),
    }
}

好处是:当真实返回类型特别复杂(比如闭包、迭代器的类型只有编译器知道),你写不出具体类型名,就用 impl Trait 告诉调用者”返回的东西实现了这个 trait”就行。

Warning

impl Trait 作返回值有个硬限制:只能返回一个具体类型。下面这样编不过,因为 if/else 返回了 PostWeibo 两个不同的类型:

fn f(switch: bool) -> impl Summary {
    if switch { Post { /* ... */ } } else { Weibo { /* ... */ } }
}

想返回不同具体类型,得用后面章节讲的 trait 对象(Box<dyn Trait>)。

1-7 用约束修好 largest 函数

前面讲泛型时留下的 largest 函数,加上 trait 约束后就通了:

fn largest<T: PartialOrd + Copy>(list: &[T]) -> T {
    let mut largest = list[0];
    for &item in list.iter() {
        if item > largest {
            largest = item;
        }
    }
    largest
}

PartialOrd> 比较合法,Copylist[0] 能直接复制出来(不必搬动所有权)。如果嫌 Copy 限制太死,可以改用 Clone 或干脆返回引用 &T——那样连 Copy/Clone 都不用,还没有堆分配。

1-8 用约束驱动一个泛型算法

把前面学的”默认实现 + trait 约束”合起来,就能写出既通用又不重复代码的算法。下面这个函数要求元素可比较、可打印,并自带一个默认就能用的摘要方法:

use std::fmt::Debug;

trait Summarize {
    fn title(&self) -> String;          // 必须自己实现
    fn summary(&self) -> String {        // 有默认实现
        format!("(摘要){}", self.title())
    }
}

fn report<T: Summarize + Debug>(items: &[T]) {
    for it in items {
        println!("{:?} -> {}", it, it.summary());
    }
}

reportT: Summarize + Debug 同时约束两个 trait;若约束更长,可换成 where T: Summarize + Debug。注意 impl TraitT: Trait 在这里等价:把参数写成 items: &[impl Summarize + Debug] 也行,只是不能再在 where 里复用这个名字。

Note

“条件实现”是这里的进阶点:当你写 impl<T: Display> Wrapper<T> 时,只有满足 DisplayT 才拥有这个 impl 里的方法;不满足的类型调用该方法会编译失败。约束,决定了”哪些方法存在”。

1-9 trait 约束 vs 动态分发 dyn

学到这里要分清两条路。一条是”泛型 + trait 约束”(静态分发):编译器为每种具体类型各生成一份代码,零运行时开销,但每个不同类型会撑大二进制体积。另一条是”特征对象 dyn Trait”(动态分发):用一块胖指针指向”数据 + 函数表”,运行时查表调用,灵活但有一点点间接开销。

// 静态:每个 T 各生成一份,零成本
fn show_static<T: std::fmt::Debug>(x: T) { println!("{:?}", x); }

// 动态:收任何实现了 Debug 的类型,放进同一个 Vec
fn show_dyn(items: &[&dyn std::fmt::Debug]) {
    for it in items { println!("{:?}", it); }
}

当你需要”把不同类型装进同一个集合、统一处理”(比如一个 Vec 里既有 i32 又有 String),静态分发做不到,就得用 dyn Trait。入门阶段优先用泛型约束;遇到”异构集合”需求再上 dyn

Note

不是所有 trait 都能当 dyn 用(要满足”对象安全”,例如不能有返回 Self 的方法),这是进阶话题;记住”泛型求性能、dyn 求灵活”这个总纲即可。

1-10 把泛型 largest 完整走一遍

前面零散提到了”找最大元素”的例子,这里把它收完整,正好串起本章所有知识点。我们要写一个 largest,返回切片里最大的元素:

fn largest<T: PartialOrd + Copy>(list: &[T]) -> T {
    let mut max = list[0];
    for &item in list {
        if item > max { max = item; }
    }
    max
}

约束 T: PartialOrd 是因为比较大小要用 >;约束 T: Copy 是因为我们直接把元素”复制”出来当返回值(否则要改成返回引用 &T,约束换成 T: PartialOrd,返回类型 &T)。这就是”约束决定方法能不能用”的活例子:没有 PartialOrd> 编译不过;没有 Copy,返回所有权会报错。

Tip

当你写泛型函数报”这个方法找不到”时,先想”这个方法要求类型满足什么 trait”,再把那个 trait 加进约束。约束不是装饰,它是”我能在 T 上做什么”的契约。

1-11 默认实现是约定,不是偷懒

有人觉得”默认实现”只是省打字,其实它更重要的是”立约定”。当一个 trait 提供了默认方法,就意味着所有实现者都自动拥有这套行为,行为保持一致;而必须自己实现的方法,才是每个类型”不可替代”的个性。

举个小例子:一个 Drawable trait 要求实现 draw,同时提供默认的 draw_all 去画一组:

trait Drawable {
    fn draw(&self);                 // 每个类型自己画
    fn draw_all(items: &[&dyn Drawable]) {  // 默认:挨个画
        for it in items { it.draw(); }
    }
}

这样,任何实现了 draw 的类型都免费获得 draw_all,且”怎么画一组”的逻辑只有一份、不会因人而异。反过来,如果一个方法你”不希望别人用默认版本”,就别写默认实现,强制每个类型自己给——默认实现应当留给”确有通用写法”的方法。

Tip

设计 trait 时自问:这个方法有没有”对所有类型都成立的通用实现”?有,就给默认;没有,就留成必须实现的抽象。

1-12 几种写法的取舍

把本章出现的约束写法收个拾,免得你下次纠结用哪个:

想要的效果写法
参数”实现了某 trait 即可”,不在意具体类型&impl Trait
多个参数必须是同一类型,且该类型实现 traitfn f<T: Trait>(a: &T, b: &T)
一个类型要同时满足多个 traitT: A + B&(impl A + B)
约束很长、签名读着累where 子句挪到后面
返回值”实现了某 trait”,但只有一种具体类型-> impl Trait
把不同类型装进同一个集合统一处理&dyn Trait(动态分发)

一句话:静态分发(泛型约束)求性能,dyn Trait 求灵活。入门阶段,默认用泛型约束 + impl Trait,遇到”异构集合”或”运行时才能决定类型”再考虑 dyn。记住约束不是装饰——它是你和编译器之间的契约:“我能在 T 上做哪些操作”。

Tip

写泛型函数报”找不到某个方法”时,第一反应就是:这个方法要求 T 满足什么 trait?把它加进约束。这条经验能解掉你九成的泛型报错。

1-13 小结

  • trait 方法可写默认实现,且默认实现能调用同 trait 的其他方法;
  • impl TraitT: Trait 的语法糖,强制同类型只能用 trait bound;
  • 多重约束用 +;约束多时用 where 提升可读性;
  • 约束能做条件实现:满足约束才有方法/trait;
  • impl Trait 可作返回值,但只能对应一个具体类型;
  • 泛型约束 + 默认实现,是 Rust 表达抽象的核心组合拳。

下一章进入生命周期,理解 Rust 如何在编译期保证引用不会悬空。