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

像 class 一样思考:Rust 的面向对象

Rust 没有 class,却能用 struct + impl + trait 表达封装、构造、接口、多态与继承。

像 class 一样思考:Rust 的面向对象

你会学到什么

从 Java / C++ / Python / TypeScript 转过来的人,最常问的一句是:“Rust 的 class 在哪?” 答案是:Rust 没有 class 关键字,但「类」的每一项能力都能用 struct + impl + trait 组合出来,而且拆得更清楚:

  • 数据放进 struct(字段默认私有,天然封装)。
  • 行为放进 impl 块(方法 / 构造器 / 静态成员)。
  • 契约放进 trait(接口 + 可选默认实现)。
  • 多态用泛型(静态分发)或 dyn(动态分发)。
  • 复用用「组合 + 委托」,而不是继承。

概念对照表

其它语言里的概念Rust 里的写法
class 的字段struct 的字段(默认私有)
class 的方法impl 块里带 &self / &mut self 的函数
构造函数 new X()关联函数 fn new(...) -> Self(约定名 new)
静态方法 / 静态常量不带 self 的关联函数 / 关联常量 const
toString()实现标准库 Display
interface / 抽象方法trait 里没有方法体的方法
抽象基类的具体方法trait 里的默认方法
多态(虚函数)泛型 impl TraitBox<dyn Trait>
继承(extends)组合 + 委托,或 supertrait 复用契约
@dataclass / 自动 equals#[derive(Debug, Clone, PartialEq)]

运行代码

cd examples
cargo run -p rt_43_class_like
cargo test -p rt_43_class_like

这个示例特意拆成了多个文件,演示真实项目的代码组织(而不是把所有东西塞进一个 main.rs):

src/
├── lib.rs            // 模块声明 + 对外重新导出
├── account.rs        // BankAccount:封装、构造器、Display
├── account_trait.rs  // Account trait:接口 + 默认方法
├── savings.rs        // SavingsAccount:组合代替继承
├── polymorphism.rs   // 静态分发 / 动态分发
└── main.rs           // 仅负责跑演示,use 库里的类型

完整源码已嵌入本页底部,建议对照下面的讲解逐段阅读。

代码讲解

1. 封装:struct 存数据,impl 放方法

字段不写 pub 就是私有的——模块外拿不到它,只能通过你提供的方法访问。于是「余额不能为负」这种不变量就能被牢牢守住:外部无法直接改 balance,只能走 deposit / withdraw

pub struct BankAccount {
    owner: String,
    balance: i64, // 私有:外部不能直接改
}

impl BankAccount {
    pub fn balance(&self) -> i64 { self.balance }      // getter:&self 只读
    pub fn deposit(&mut self, amount: i64) {           // &mut self 可改
        self.balance += amount;
    }
}

&self&mut selfself 分别对应「只读方法、可变方法、消耗自身的方法」,比其它语言「方法默认都能改对象」更精确。

2. 构造器与静态成员:关联函数 + 关联常量

Rust 没有专门的构造函数语法。不带 self 的关联函数就是构造器,约定叫 new;它还能返回 Result,把非法对象挡在诞生之前——这是普通构造函数做不到的。

impl BankAccount {
    pub const MIN_OPENING: i64 = 100; // 关联常量 = 静态常量

    pub fn new(owner: &str, opening: i64) -> Result<Self, String> {
        if opening < Self::MIN_OPENING {
            return Err(format!("开户金额至少 {} 分", Self::MIN_OPENING));
        }
        Ok(Self { owner: owner.to_string(), balance: opening })
    }
}

调用时用 BankAccount::new(...)BankAccount::MIN_OPENING,就像调用「类的静态成员」。

3. toString:实现 Display

不要自己写 to_string 方法。实现标准库的 Display,就自动获得 {} 格式化和 .to_string():

impl std::fmt::Display for BankAccount {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        write!(f, "{} 的账户:{:.2} 元", self.owner, self.balance as f64 / 100.0)
    }
}

4. 接口与抽象基类:trait

trait 同时扮演「interface」和「带默认实现的抽象基类」。没有方法体的是必须实现的抽象方法;有方法体的是默认方法,实现者不重写就直接用:

pub trait Account {
    fn balance(&self) -> i64;          // 抽象方法,必须实现
    fn kind(&self) -> &str;            // 抽象方法,必须实现
    fn summary(&self) -> String {      // 默认方法,可不重写
        format!("[{}] 余额 {} 分", self.kind(), self.balance())
    }
}

BankAccount 实现它时只写两个抽象方法,summary 用默认版;SavingsAccount重写(override)了 summary

5. 继承怎么办?用组合代替

这是最关键、也最反直觉的一点:Rust 不能继承 struct。想复用 BankAccount,就把它作为字段「包」进新类型里(组合),再把需要的调用委托过去:

pub struct SavingsAccount {
    base: BankAccount, // 组合:拥有一个,而不是“是一个”
    rate_bps: i64,
}

impl SavingsAccount {
    pub fn accrue_interest(&mut self) {                 // 新增能力
        let interest = self.base.balance() * self.rate_bps / 10_000;
        self.base.deposit(interest);
    }
    pub fn deposit(&mut self, amount: i64) {            // 委托给“父类”
        self.base.deposit(amount);
    }
}

业界共识「组合优于继承」在 Rust 里被直接写进了语言:你不会继承到一堆用不上的方法,也不会被脆弱的基类层级绑死。需要共享契约而非实现时,用 supertrait(trait B: A {})即可。

6. 多态:静态分发 vs 动态分发

让同一段逻辑作用于不同类型,有两条路:

  • 泛型(静态分发):fn f(x: &impl Account),编译期为每个具体类型生成专门代码,零运行时开销。
  • trait 对象(动态分发):Box<dyn Account>,运行期查虚表,代价是一次间接跳转,好处是能把不同类型装进同一个 Vec
fn print_static(acc: &impl Account) { /* 单态化 */ }
fn total_balance(accounts: &[Box<dyn Account>]) -> i64 {
    accounts.iter().map(|a| a.balance()).sum()
}

常见错误

  • class / extends:它们不存在。把「数据 + 行为」拆成 struct + impl,把「能继承的部分」改成 trait + 组合。
  • 想继承 struct 来复用字段:做不到。用组合(把旧类型当字段)+ 委托,或用 trait 共享行为契约。
  • 给 trait 对象写返回 Self 的方法:dyn Trait 要求方法「对象安全」,返回 Self、泛型方法都会让 trait 不能做成 dyn。需要时改用泛型,或返回 Box<dyn Trait>
  • 自己写 to_string():实现 Display 即可,别重复造轮子;需要调试输出用 #[derive(Debug)] + {:?}

练习

  • Account 加一个默认方法 is_empty(&self) -> bool(余额为 0),让两个类型都自动获得它。
  • 新增一个 CheckingAccount(支票账户,允许小额透支),用组合复用 BankAccount,并实现 Account,放进同一个 Vec<Box<dyn Account>> 里。
  • print_static 改写成接收 Vec<Box<dyn Account>> 的版本,对比泛型与 trait 对象在「同质集合 vs 异质集合」上的差异。
  • 定义 trait Taxable: Account {}(supertrait),只有实现了 Account 的类型才能实现它,体会「用 trait 表达契约继承」。

小结

Rust 用三件套覆盖了「类」的全部职责:struct 管数据与封装,impl 管方法与构造,trait 管接口与多态;继承则由「组合 + 委托」和 supertrait 取代。拆开之后,封装更严、复用更灵活,也更难写出脆弱的类层级。

下一步

掌握了如何用类型和 trait 建模后,下一章进入闭包——把「行为」当作值来传递。

完整示例代码

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

examples/43_class_like/src/main.rs
//! 运行各模块的小演示。真正的实现都在库 crate(见 `src/lib.rs` 及其模块)。

use rt_43_class_like::{Account, BankAccount, SavingsAccount, print_static, total_balance};

fn main() {
    println!("== 1. 封装:构造器 + 方法维护不变量 ==");
    let mut acc = BankAccount::new("阿俊", 10_000).expect("开户成功");
    acc.deposit(5_000);
    match acc.withdraw(3_000) {
        Ok(()) => println!("  取款成功,余额 {}", acc.balance()),
        Err(e) => println!("  取款失败:{e}"),
    }
    if let Err(e) = acc.withdraw(999_999) {
        println!("  非法取款被拦截:{e}");
    }
    // Display:直接用 {} 打印,等价于其它语言的 toString。
    println!("  Display -> {acc}");

    println!("== 2. 关联常量当作静态常量使用 ==");
    println!("  最低开户金额 = {} 分", BankAccount::MIN_OPENING);

    println!("== 3. 组合代替继承:储蓄账户计息 ==");
    let mut savings = SavingsAccount::new("小美", 100_000, 250).expect("开户成功");
    savings.accrue_interest();
    println!("  计息后:{}", savings.summary());

    println!("== 4. 多态:静态分发 vs 动态分发 ==");
    print_static(&acc);
    print_static(&savings);
    let accounts: Vec<Box<dyn Account>> = vec![Box::new(acc), Box::new(savings)];
    for a in &accounts {
        println!("  dynamic -> {}", a.summary());
    }
    println!("  全部账户余额合计 = {} 分", total_balance(&accounts));
}
examples/43_class_like/src/lib.rs
//! Rust 里没有 `class` 关键字,但「类」的能力都能用 struct + impl + trait 表达。
//!
//! 本 crate 按其它语言里熟悉的面向对象概念,把代码拆成几个模块逐一对照:
//! - `account`:封装 + 构造器 + 关联常量 + Display(等价 toString)。
//! - `account_trait`:接口 / 抽象基类(trait + 默认方法)。
//! - `savings`:用「组合 + 委托」代替继承。
//! - `polymorphism`:静态分发(泛型)与动态分发(`dyn`)。

pub mod account;
pub mod account_trait;
pub mod polymorphism;
pub mod savings;

// 重新导出常用类型,调用方可以直接 `use rt_43_class_like::BankAccount`。
pub use account::BankAccount;
pub use account_trait::Account;
pub use polymorphism::{print_static, total_balance};
pub use savings::SavingsAccount;
examples/43_class_like/src/account_trait.rs
//! 接口 / 抽象基类:trait。
//!
//! trait 既能像 interface 规定「必须实现的方法」,
//! 也能像抽象基类提供「默认实现」。

/// 「账户」契约。任何想被当作账户对待的类型都要实现它。
pub trait Account {
    /// 抽象方法:必须实现。
    fn balance(&self) -> i64;
    /// 抽象方法:必须实现。
    fn kind(&self) -> &str;

    /// 默认方法:不重写就用这份实现(类似抽象基类里的具体方法)。
    fn summary(&self) -> String {
        format!("[{}] 余额 {} 分", self.kind(), self.balance())
    }
}
examples/43_class_like/src/account.rs
//! 封装:struct 存字段(默认私有)+ impl 提供方法。
//!
//! 字段不加 `pub` 默认私有,模块外只能通过方法访问,
//! 等价于其它语言把字段设为 private、再提供 getter / setter。

use std::fmt;

use crate::account_trait::Account;

/// 一个银行账户。余额以「分」为单位存储,且保证永不为负 —— 这条不变量
/// 由方法来维护,外部拿不到可变字段,自然无法破坏它。
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct BankAccount {
    owner: String,
    balance: i64,
}

impl BankAccount {
    /// 关联常量:类似「类的静态常量」。用 `BankAccount::MIN_OPENING` 访问。
    pub const MIN_OPENING: i64 = 100;

    /// 关联函数(无 `self`)= 构造器 / 静态工厂方法。
    /// 返回 `Result`,把「开户金额不足」这种非法状态挡在对象诞生之前。
    pub fn new(owner: &str, opening: i64) -> Result<Self, String> {
        if opening < Self::MIN_OPENING {
            return Err(format!("开户金额至少 {} 分", Self::MIN_OPENING));
        }
        Ok(Self {
            owner: owner.to_string(),
            balance: opening,
        })
    }

    /// `&self`:只读 getter。返回借用,避免不必要的克隆。
    pub fn owner(&self) -> &str {
        &self.owner
    }

    /// `&mut self`:会修改自身的实例方法。
    pub fn deposit(&mut self, amount: i64) {
        self.balance += amount;
    }

    /// `&mut self`:取款时维护「余额不为负」的不变量,违反就返回错误。
    pub fn withdraw(&mut self, amount: i64) -> Result<(), String> {
        if amount > self.balance {
            return Err(format!("余额不足:想取 {amount},仅有 {}", self.balance));
        }
        self.balance -= amount;
        Ok(())
    }
}

/// toString:实现 `Display`,而不是写一个 `to_string` 方法。
/// 实现了 Display 就自动获得 `.to_string()` 和 `{}` 格式化能力。
impl fmt::Display for BankAccount {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        write!(
            f,
            "{} 的账户:{:.2} 元",
            self.owner,
            self.balance as f64 / 100.0
        )
    }
}

impl Account for BankAccount {
    fn balance(&self) -> i64 {
        self.balance
    }

    fn kind(&self) -> &str {
        "普通账户"
    }
    // 不重写 summary,直接用 trait 的默认实现。
}

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

    #[test]
    fn rejects_low_opening() {
        assert!(BankAccount::new("阿俊", 50).is_err());
    }

    #[test]
    fn deposit_and_withdraw_keep_invariant() {
        let mut acc = BankAccount::new("阿俊", 1_000).unwrap();
        acc.deposit(500);
        assert_eq!(acc.balance(), 1_500);
        assert!(acc.withdraw(2_000).is_err()); // 不变量:不能透支
        assert!(acc.withdraw(1_000).is_ok());
        assert_eq!(acc.balance(), 500);
    }

    #[test]
    fn display_formats_yuan() {
        let acc = BankAccount::new("阿俊", 12_345).unwrap();
        assert_eq!(acc.to_string(), "阿俊 的账户:123.45 元");
    }

    #[test]
    fn default_trait_method_used_by_bank_account() {
        let acc = BankAccount::new("阿俊", 1_000).unwrap();
        assert_eq!(acc.summary(), "[普通账户] 余额 1000 分");
    }
}
examples/43_class_like/src/polymorphism.rs
//! 多态:同一段逻辑作用于不同类型。

use crate::account_trait::Account;

/// 静态分发(泛型):编译期为每个具体类型生成专门代码,零运行时开销。
pub fn print_static(acc: &impl Account) {
    println!("  static  -> {}", acc.summary());
}

/// 动态分发(trait 对象):运行期通过虚表调用,代价是一次间接跳转,
/// 好处是能把「不同的具体类型」装进同一个集合里。
pub fn total_balance(accounts: &[Box<dyn Account>]) -> i64 {
    accounts.iter().map(|a| a.balance()).sum()
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::account::BankAccount;
    use crate::savings::SavingsAccount;

    #[test]
    fn polymorphism_over_trait_objects() {
        let accounts: Vec<Box<dyn Account>> = vec![
            Box::new(BankAccount::new("阿俊", 1_000).unwrap()),
            Box::new(SavingsAccount::new("小美", 2_000, 100).unwrap()),
        ];
        assert_eq!(total_balance(&accounts), 3_000);
    }
}
examples/43_class_like/src/savings.rs
//! 「继承」用组合代替:composition over inheritance。
//!
//! Rust 不能继承 struct。要复用 BankAccount 的能力,就把它作为字段
//! 「包」进来,新增自己的字段/方法,并把需要的调用「委托」过去。

use crate::account::BankAccount;
use crate::account_trait::Account;

/// 储蓄账户:在普通账户之上增加「计息」能力。
pub struct SavingsAccount {
    base: BankAccount, // 组合:拥有一个 BankAccount,而非继承它
    rate_bps: i64,     // 年利率,单位:基点(1% = 100bps)
}

impl SavingsAccount {
    pub fn new(owner: &str, opening: i64, rate_bps: i64) -> Result<Self, String> {
        Ok(Self {
            base: BankAccount::new(owner, opening)?, // 复用父类型的构造器
            rate_bps,
        })
    }

    /// 新增能力:按利率结算一次利息。
    pub fn accrue_interest(&mut self) {
        let interest = self.base.balance() * self.rate_bps / 10_000;
        self.base.deposit(interest);
    }

    /// 委托:把基础能力转发给内部的 BankAccount(等价于调用「父类方法」)。
    pub fn deposit(&mut self, amount: i64) {
        self.base.deposit(amount);
    }
}

// 让「子类型」也满足同一个接口 —— 通过委托实现 trait,
// 于是 SavingsAccount 能和 BankAccount 一起被多态地使用。
impl Account for SavingsAccount {
    fn balance(&self) -> i64 {
        self.base.balance()
    }

    fn kind(&self) -> &str {
        "储蓄账户"
    }

    /// 重写(覆盖)默认方法 —— 类似子类 override 基类方法。
    fn summary(&self) -> String {
        format!(
            "[{}] 余额 {} 分,年化 {}bps",
            self.kind(),
            self.balance(),
            self.rate_bps
        )
    }
}

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

    #[test]
    fn savings_overrides_summary_and_accrues() {
        let mut savings = SavingsAccount::new("小美", 100_000, 250).unwrap();
        savings.accrue_interest(); // 100000 * 250 / 10000 = 2500
        assert_eq!(savings.balance(), 102_500);
        assert!(savings.summary().contains("储蓄账户"));
        assert!(savings.summary().contains("250bps"));
    }
}
examples/43_class_like/Cargo.toml
[package]
name = "rt_43_class_like"
version.workspace = true
edition.workspace = true
publish.workspace = true