面向对象特性:trait 对象与状态模式 — 第 18 章拆解
面向对象特性:trait 对象与状态模式
这一章讲什么: 三个 OOP 特征在 Rust 里的判决——封装:✅(pub 私有默认); 多态:✅ 但走 trait 对象(动态分发)或泛型(静态分发)两条路; 继承:❌ 语言里没有,trait 默认实现和组合是替代品。 落点是一次设计对决:把状态编进类型系统,能让非法状态编译不过——这是 OOP 语言给不了的。
1. 三个特征的判决
特征一:对象(数据+方法)。 按 GoF《设计模式》的定义,对象=数据+操作数据的过程;结构体/枚举装数据、impl 块供方法,按这个定义 Rust 是面向对象的1。
特征二:封装。 pub 标记公开、其余默认私有——AveragedCollection 例子:list 和 average 字段私有,外部只能走 add/remove/average 方法,不变量(均值永远同步)由方法维护;将来把 Vec 换成 HashSet,公开签名不变,用户代码不动2。
特征三:继承。 Rust 没有继承——无法让结构体继承父结构体的字段和方法。要继承通常为了两件事,各有替代:
- 代码复用 → trait 默认实现:实现者免费拿到方法,可覆盖——形态上就是"父类实现 + 子类覆盖"。
- 类型系统的多态(子类能用在父类的地方)→ trait 对象。 书里还给了 Rust 拒绝继承的理由:继承常常共享过度(子类被迫继承不需要的特征)、只能单继承进一步限制设计3。
2. trait 对象:运行时的多态
GUI 主线问题:Screen 要装一堆"能 draw 的不同类型"(Button、TextField、还有用户将来自己加的 Image)——编译期不知道全集。解法:存 trait 对象。
pub trait Draw { fn draw(&self); }
pub struct Screen {
pub components: Vec<Box<dyn Draw>>, // trait 对象:指针 + 运行时方法查找表
}trait 对象指向两样东西:一个实现了指定 trait 的实例,和一张在运行时查方法的表。写法:某种指针(&、Box 等)+ dyn + trait 名。 类型系统保证:凡是放进这个上下文里的值必然实现了 Draw——所以不必编译期知道全部类型4。
和泛型的关键区别(Screen<T: Draw> 是替代方案):泛型参数一次只能填一种具体类型(一个 Screen 全是 Button);trait 对象允许同一个 Vec 里混装多种类型——异质集合是它的主场。代价:动态分发——编译器在运行时查方法表才能决定调谁,有查表开销,还挡住了内联和部分优化(静态分发的泛型走单态化,零开销)5。鸭子类型在编译期被锁死:往 Screen 里塞没实现 Draw 的 String,E0277 直接拒——动态语言的"跑到一半发现没这方法"不存在6。
3. 对决:状态模式的两版实现
工作流:草稿 → 请求评审 → 审批发布;只有已发布文章能读出内容;非法操作(直接批准草稿)无效果。
版本 A:教科书 OOP 状态模式。 Post { state: Option<Box<dyn State>>, content };State 是私有 trait,每种状态一个结构体(Draft/PendingReview/Published)。三个关键手法:
- 状态转换消耗旧状态:State 的方法签名是
fn request_review(self: Box<Self>) -> Box<dyn State>——self: Box<Self>表示方法只在 Box 上可调,调用即拿走旧状态的所有权;Draft 返回新 PendingReview,PendingReview 返回自己(重复请求评审=无效果)。 Option::take()移状态:Rust 不允许结构体字段空着,所以先 take 出 Some、留 None、算好新状态再填回——顺带保证转换期间 Post 拿不到旧状态。- 行为委托:Post::content 委托给状态的 content(默认实现返回 "",Published 覆盖为 &post.content)。
收益:Post 的方法不再认识任何具体状态,加新状态=加一个结构体。代价书里也如实列了:状态互相耦合(插入 Scheduled 就得改 PendingReview)、转换样板重复、以及 content 逻辑跨在 State 和 Post 之间的职责模糊7。
版本 B:把状态编码进类型。 Post / DraftPost / PendingReviewPost 是三个不同的结构体:Post::new 返回 DraftPost(它只有 add_text,没有 content 方法);request_review(self) -> PendingReviewPost(也没有 content);approve(self) -> Post(有 content)。转换消耗 self——旧状态的对象物理消失。"显示草稿内容"从"返回空字符串的运行时行为"变成"方法不存在,编译不过";工作流被钉进了类型系统8。
4. 作者的判断与证据
- "按 GoF 定义 Rust 是 OO;按继承定义不是"——条件句,作者不给唯一判决13。
- "trait 对象换灵活性,代价是运行时开销"——机制事实(动态分发 vs 单态化)5。
- "OOP 模式在 Rust 可行,但常常不是利用 Rust 强项的最佳方式;所有权这类特征是 OO 语言没有的"——本章的总结论9。
判断(我们的,不是书里的): 版本 B 展示的才是 Rust 的真正态度: 能用类型系统说清的事,就别留给运行时纪律。 OOP 状态模式把"非法操作无效果"当运行时规则维护; 类型编码版本让非法操作不存在。这不是"Rust 版的状态模式",是另一种设计哲学—— 书选择以两种实现并列收尾而不是宣布谁赢,是对的:异质可扩展的集合(GUI)仍需要 trait 对象, 工具箱里两件都是真家伙,判据是"非法状态要多贵才拦得住"。 如果错,会错在: 类型编码版本的代价是状态越多类型越多、转换链写死在签名里, 对状态多且转换自由的领域(如编译器的 IR 变换),trait 对象版本的运行时灵活性可能反而值钱; 判据不是"类型编码永远更好",而是"出错代价 × 状态数量"的权衡。
5. 边界与局限
- dyn compatibility(dyn 可用性的完整规则)只给了名字和参考链接。
- trait 对象不能加数据(只有行为),书里明确提醒它不如其他语言的对象通用4。
- 版本 B 的重复(struct 三个 + 字段私有逐份)没有进一步用宏或泛型压缩。
6. 可带走的
- 按 GoF 定义 Rust 是 OO(数据+方法);封装由 pub/默认私有实现。
- 无继承;代码复用走 trait 默认实现,多态走 trait 对象。
Box<dyn Trait>是 trait 对象:指针 + 运行时方法表;异质集合用它。- 泛型 = 静态分发(单态化、零开销、同质集合);trait 对象 = 动态分发(灵活、有查表开销、异质集合)。
- 未实现 trait 的值进不了 trait 对象(E0277),鸭子类型被编译期锁死。
- 状态模式 OOP 版:
Option<Box<dyn State>>+self: Box<Self>消耗式转换 +Option::take。 - 类型编码版:每个状态一个类型,转换消耗 self;非法状态=编译错误。
- "非法操作无效果"是运行时纪律;"非法操作不存在"是类型系统——后者是 Rust 的偏好。
7. 原文地图
| 主题 | 原书章 | 原文位置 |
|---|---|---|
| GoF 定义与对象 | Chapter 18: Object-Oriented Programming Features | text/25-ch18-chapter-18-object-oriented-programming-features.txt:31(搜「Gang of Four」) |
| 封装与 AveragedCollection | 同上 | text/25-ch18-chapter-18-object-oriented-programming-features.txt:51(搜「pub keyword」) · text/25-ch18-chapter-18-object-oriented-programming-features.txt:113(搜「encapsulated the implementation」) |
| 无继承与两个替代 | 同上 | text/25-ch18-chapter-18-object-oriented-programming-features.txt:132(搜「no way to define a struct」) · text/25-ch18-chapter-18-object-oriented-programming-features.txt:140(搜「default trait method」) |
| trait 对象定义 | 同上 | text/25-ch18-chapter-18-object-oriented-programming-features.txt:212(搜「table used to look up」) |
| 泛型 vs trait 对象 | 同上 | text/25-ch18-chapter-18-object-oriented-programming-features.txt:266(搜「only one concrete type」) |
| E0277 鸭子类型锁死 | 同上 | text/25-ch18-chapter-18-object-oriented-programming-features.txt:413(搜「trait bound」) |
| 动态分发代价 | 同上 | text/25-ch18-chapter-18-object-oriented-programming-features.txt:427(搜「Dynamic Dispatch」) |
| 状态模式 OOP 版 | 同上 | text/25-ch18-chapter-18-object-oriented-programming-features.txt:633(搜「self: Box<Self>」) · text/25-ch18-chapter-18-object-oriented-programming-features.txt:663(搜「take method」) |
| 状态模式评估 | 同上 | text/25-ch18-chapter-18-object-oriented-programming-features.txt:845(搜「coupled to each」) |
| 类型编码状态 | 同上 | text/25-ch18-chapter-18-object-oriented-programming-features.txt:896(搜「pub struct DraftPost」) · text/25-ch18-chapter-18-object-oriented-programming-features.txt:973(搜「encoded the blog」) |
Footnotes
-
出处:「Chapter 18: Object-Oriented Programming Features」第 31 段(
text/25-ch18-chapter-18-object-oriented-programming-features.txt:31,搜「Gang of Four」)。 ↩ ↩2 -
出处:「Chapter 18: Object-Oriented Programming Features」第 113 段(
text/25-ch18-chapter-18-object-oriented-programming-features.txt:113,搜「encapsulated the implementation」)。 ↩ -
出处:「Chapter 18: Object-Oriented Programming Features」第 132 段(
text/25-ch18-chapter-18-object-oriented-programming-features.txt:132,搜「no way to define a struct」)与第 163 段(text/25-ch18-chapter-18-object-oriented-programming-features.txt:163,搜「chosen a different set of trade-offs」)。 ↩ ↩2 -
出处:「Chapter 18: Object-Oriented Programming Features」第 212 段(
text/25-ch18-chapter-18-object-oriented-programming-features.txt:212,搜「table used to look up」)。 ↩ ↩2 -
出处:「Chapter 18: Object-Oriented Programming Features」第 266 段(
text/25-ch18-chapter-18-object-oriented-programming-features.txt:266,搜「only one concrete type」)与第 427 段(text/25-ch18-chapter-18-object-oriented-programming-features.txt:427,搜「Dynamic Dispatch」)。 ↩ ↩2 -
出处:「Chapter 18: Object-Oriented Programming Features」第 413 段(
text/25-ch18-chapter-18-object-oriented-programming-features.txt:413,搜「trait bound」)。 ↩ -
出处:「Chapter 18: Object-Oriented Programming Features」第 633 段(
text/25-ch18-chapter-18-object-oriented-programming-features.txt:633,搜「self: Box<Self>」)与第 845 段(text/25-ch18-chapter-18-object-oriented-programming-features.txt:845,搜「coupled to each」)。 ↩ -
出处:「Chapter 18: Object-Oriented Programming Features」第 889 段(
text/25-ch18-chapter-18-object-oriented-programming-features.txt:889,搜「DraftPost」)与第 973 段(text/25-ch18-chapter-18-object-oriented-programming-features.txt:973,搜「encoded the blog」)。 ↩ -
出处:「Chapter 18: Object-Oriented Programming Features」第 1017 段(
text/25-ch18-chapter-18-object-oriented-programming-features.txt:1017,搜「won’t always be the best」)。 ↩