像 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 Trait 或 Box<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 self、self 分别对应「只读方法、可变方法、消耗自身的方法」,比其它语言「方法默认都能改对象」更精确。
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