泛型、trait 与闭包 intermediate 30 分钟 更新 2026-06-15

trait 与共享行为

用 trait 定义共享接口和默认方法。

trait 与共享行为

你会学到什么

  • trait 定义一组类型可以共享的方法(类似接口)。
  • 为多个类型实现同一个 trait。
  • trait 可以提供默认方法,实现者按需覆盖。
  • 把 trait 作为泛型函数的约束(trait bound)。
  • 参数位置与返回位置的 impl Trait
  • 为自定义类型实现标准库 trait(std::fmt::Display)。

本章聚焦“静态分发”(编译期单态化)。运行时多态用的 dyn trait 对象留到下一章。

最小示例

trait Summary {
    fn summarize(&self) -> String;
    fn preview(&self) -> String {
        format!("{} (点击查看更多)", self.summarize()) // 默认方法
    }
}

运行代码

cd examples
cargo run -p rt_17_traits
cargo test -p rt_17_traits

代码组织

本章示例按职责拆成了多个文件,更贴近真实项目的组织方式:

17_traits/
├── Cargo.toml
└── src/
    ├── lib.rs      # mod 声明 + pub use 重导出 + 库级文档
    ├── summary.rs  # Summary trait、Article/Tweet 及操作它们的函数
    ├── shape.rs    # Shape trait、Circle/Rectangle、Display 实现及相关函数
    └── main.rs     # 瘦入口:use 库里的类型,跑一遍演示

每个模块都带有自己的单元测试(#[cfg(test)] mod tests),测试就近放在它所覆盖的代码旁边。注意调用 trait 方法时,需要把对应的 trait(如 Summary)引入作用域。

代码讲解

为多个类型实现同一个 trait

Summary 提供了一个必须实现的 summarize,外加两个默认方法 previewword_countArticle 不覆盖任何默认方法,于是自动获得默认实现;Tweet 则覆盖了 preview,因为它本身已经足够简短。默认方法可以调用 self.summarize(), 所以即便实现者只写一行,也能复用完整逻辑。

trait bound:三种等价写法

约束泛型“必须实现某个 trait”有几种写法,挑顺手的即可:

fn announce<T: Summary>(item: &T) -> String { /* 泛型 + trait bound */ }
fn headline(item: &impl Summary) -> String { /* 参数位置 impl Trait,语法糖 */ }

泛型写法 <T: Summary> 能在多个参数间共享同一个 T,表达力最强;&impl Summary 更简洁。两者都是编译期单态化,零运行时开销。

返回位置的 impl Trait

返回 impl Summary 时,调用方只知道“这是一个 Summary”,不必关心具体类型:

fn featured() -> impl Summary {
    Tweet { /* ... */ }
}

这在返回闭包、迭代器等“类型名难以书写”的场景特别有用。

为自定义类型实现标准库 trait

Shape 演示了用 trait bound 写通用算法(describe_shape)。同时我们给 Rectangle 实现了标准库的 std::fmt::Display,于是它可以被 {} 直接打印:

impl std::fmt::Display for Rectangle {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        write!(f, "{}×{} 的矩形", self.width, self.height)
    }
}

因为 Rectangle 定义在本 crate,满足孤儿规则,所以允许这样实现。

常见错误

试图为外部类型实现外部 trait(孤儿规则):

impl std::fmt::Display for Vec<i32> { /* ❌ 两者都不是本地定义 */ }

trait 或类型至少有一个定义在你自己的 crate 里才允许实现。

练习

  • 定义一个 Describe trait,给 i32String 各实现一份。
  • 再加一个 Triangle 实现 Shape,并让它也实现 Display
  • 写一个返回 impl Shape 的函数,根据参数返回圆形或矩形(提示:返回位置的 impl Trait 要求所有分支是同一具体类型,思考为什么,这正是下一章 dyn 要解决的问题)。

小结

trait 是 Rust 的抽象基石:定义共享行为、提供默认实现、配合泛型做静态分发。

下一步

当需要在运行时混合多种实现时,要用 trait 对象。下一章学习 dyn

完整示例代码

下面是 examples/17_traits/ 的完整源码。无需 clone 仓库,直接在页面上阅读、复制、对照运行。

examples/17_traits/src/main.rs
//! 演示入口:调用库里拆分好的 `summary` 与 `shape` 模块,跑一遍带标签的输出。

// 注意:调用 trait 方法(如 summarize / area)需要把对应 trait 引入作用域。
use rt_17_traits::{
    Article, Circle, Rectangle, Summary, Tweet, announce, describe_shape, featured, headline,
};

fn main() {
    let article = Article {
        title: "Rust 入门".to_string(),
        author: "Alice".to_string(),
    };
    let tweet = Tweet {
        user: "bob".to_string(),
        text: "Rust 真香".to_string(),
    };

    println!("== trait bound 与默认方法 ==");
    println!("{}", announce(&article)); // 用默认 preview
    println!("{}", announce(&tweet)); // 用覆盖后的 preview
    println!("{}", headline(&article)); // impl Trait 参数 + word_count

    println!("\n== 返回位置的 impl Trait ==");
    let pick = featured();
    println!("{}", pick.summarize());

    println!("\n== Shape 与 Display ==");
    let circle = Circle { radius: 2.0 };
    let rect = Rectangle {
        width: 3.0,
        height: 4.0,
    };
    println!("{}", describe_shape(&circle));
    println!("{}", describe_shape(&rect));
    println!("Display: {rect}"); // 直接用 {} 打印
}
examples/17_traits/src/lib.rs
//! trait(特征):定义一组类型可以共享的行为,类似其他语言的“接口”。
//!
//! 本章覆盖:
//! - 定义 trait 与“必须实现”的方法;
//! - 为多个类型实现同一个 trait;
//! - 默认方法,以及在实现里覆盖默认方法;
//! - 把 trait 作为泛型函数的约束(trait bound);
//! - 参数位置与返回位置的 `impl Trait`;
//! - 为自定义类型实现标准库 trait(`std::fmt::Display`)。
//!
//! 本章聚焦“静态分发”(编译期单态化);运行时多态用的 `dyn` trait 对象
//! 留到下一章。
//!
//! 代码按职责拆分为两个模块:
//! - [`summary`]:`Summary` trait、`Article`/`Tweet` 及操作它们的函数;
//! - [`shape`]:`Shape` trait、`Circle`/`Rectangle`、`Display` 实现及相关函数。

pub mod shape;
pub mod summary;

pub use shape::{Circle, Rectangle, Shape, describe_shape};
pub use summary::{Article, Summary, Tweet, announce, featured, headline};
examples/17_traits/src/shape.rs
//! 图形相关:`Shape` trait、`Circle`/`Rectangle` 类型、`Display` 实现,
//! 以及操作 `Shape` 的函数。
//!
//! 本模块演示:
//! - 用 trait bound 写通用算法;
//! - 为自定义类型实现标准库 trait(`std::fmt::Display`)。

use std::fmt;

/// 一个带“面积”行为的 trait,演示用 trait bound 写通用算法。
pub trait Shape {
    /// 返回图形面积。
    fn area(&self) -> f64;

    /// 默认方法:名称,默认未知。
    fn name(&self) -> &str {
        "图形"
    }
}

/// 圆形。
pub struct Circle {
    pub radius: f64,
}

impl Shape for Circle {
    fn area(&self) -> f64 {
        std::f64::consts::PI * self.radius * self.radius
    }

    fn name(&self) -> &str {
        "圆形"
    }
}

/// 矩形。
pub struct Rectangle {
    pub width: f64,
    pub height: f64,
}

impl Shape for Rectangle {
    fn area(&self) -> f64 {
        self.width * self.height
    }

    fn name(&self) -> &str {
        "矩形"
    }
}

/// 为自定义类型实现标准库 trait `Display`,让它能被 `{}` 直接打印。
impl fmt::Display for Rectangle {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        write!(f, "{}×{} 的矩形", self.width, self.height)
    }
}

/// 泛型函数 + trait bound:对任意 `Shape` 给出一句描述。
pub fn describe_shape<S: Shape>(shape: &S) -> String {
    format!("{} 的面积是 {:.2}", shape.name(), shape.area())
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn shape_area_and_name() {
        let rect = Rectangle {
            width: 3.0,
            height: 4.0,
        };
        assert_eq!(rect.name(), "矩形");
        assert!((rect.area() - 12.0).abs() < 1e-9);
    }

    #[test]
    fn display_for_rectangle() {
        let rect = Rectangle {
            width: 3.0,
            height: 4.0,
        };
        assert_eq!(format!("{rect}"), "3×4 的矩形");
    }
}
examples/17_traits/src/summary.rs
//! 摘要相关:`Summary` trait、`Article`/`Tweet` 类型,以及操作 `Summary` 的函数。
//!
//! 本模块演示:
//! - 定义 trait 与“必须实现”的方法;
//! - 默认方法,以及在实现里覆盖默认方法;
//! - 把 trait 作为泛型函数的约束(trait bound);
//! - 参数位置与返回位置的 `impl Trait`。

/// 一个描述“可被摘要”行为的 trait。
pub trait Summary {
    /// 必须由实现者提供:返回内容的一行摘要。
    fn summarize(&self) -> String;

    /// 默认方法:在摘要后追加提示语。
    ///
    /// 实现者若不覆盖就直接复用这份实现,因此它能调用 `self.summarize()`。
    fn preview(&self) -> String {
        format!("{} (点击查看更多)", self.summarize())
    }

    /// 默认方法:统计摘要里的词数(按空白切分)。
    fn word_count(&self) -> usize {
        self.summarize().split_whitespace().count()
    }
}

/// 文章:拥有标题与作者。
pub struct Article {
    pub title: String,
    pub author: String,
}

impl Summary for Article {
    fn summarize(&self) -> String {
        format!("{} - {}", self.title, self.author)
    }
    // 不覆盖 preview / word_count,直接使用默认实现。
}

/// 推文:拥有用户名与正文。
pub struct Tweet {
    pub user: String,
    pub text: String,
}

impl Summary for Tweet {
    fn summarize(&self) -> String {
        format!("@{}: {}", self.user, self.text)
    }

    /// 覆盖默认实现:推文本身已足够简短,预览就等于摘要。
    fn preview(&self) -> String {
        self.summarize()
    }
}

/// 用泛型 + trait bound 接受任意实现了 `Summary` 的类型。
///
/// `<T: Summary>` 与参数位置的 `&impl Summary` 等价,但泛型写法可以在多个
/// 参数间共享同一个类型 `T`,表达力更强。
pub fn announce<T: Summary>(item: &T) -> String {
    format!("最新: {}", item.preview())
}

/// 参数位置的 `impl Trait`:是上面泛型写法的“匿名”语法糖,更简洁。
pub fn headline(item: &impl Summary) -> String {
    format!("[{} 词] {}", item.word_count(), item.summarize())
}

/// 返回位置的 `impl Trait`:调用方只知道“这是一个 Summary”,
/// 不必关心具体类型。适合返回闭包或难以书写的复杂类型。
pub fn featured() -> impl Summary {
    Tweet {
        user: "rustlang".to_string(),
        text: "Rust 2024 发布啦".to_string(),
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn uses_default_preview() {
        let article = Article {
            title: "T".to_string(),
            author: "A".to_string(),
        };
        assert_eq!(article.preview(), "T - A (点击查看更多)");
    }

    #[test]
    fn overrides_preview() {
        let tweet = Tweet {
            user: "u".to_string(),
            text: "hi".to_string(),
        };
        assert_eq!(tweet.preview(), "@u: hi");
    }

    #[test]
    fn default_word_count() {
        let tweet = Tweet {
            user: "u".to_string(),
            text: "hello world".to_string(),
        };
        // summarize 为 "@u: hello world",按空白切分得 3 个词。
        assert_eq!(tweet.word_count(), 3);
    }

    #[test]
    fn returned_impl_trait_summarizes() {
        let item = featured();
        assert_eq!(item.summarize(), "@rustlang: Rust 2024 发布啦");
    }
}
examples/17_traits/Cargo.toml
[package]
name = "rt_17_traits"
version.workspace = true
edition.workspace = true
publish.workspace = true