参考:https://kaisery.github.io/trpl-zh-cn/ch17-00-oop.html
当 Rust 程序变得更大时,有哪些符合语言习惯的问题建模方法和结构化解决方案,以及 Rust 的风格是如何与面向对象编程(Object Oriented Programming)中那些你所熟悉的概念相联系的。
面向对象编程(Object-Oriented Programming,OOP)是一种模式化编程方式。
对象(Object)来源于 20 世纪 60 年代的 Simula 编程语言。这些对象影响了 Alan Kay 的编程架构中对象之间的消息传递。他在 1967 年创造了 面向对象编程 这个术语来描述这种架构。
关于 OOP 是什么有很多相互矛盾的定义;在一些定义下,Rust 是面向对象的;在其他定义下,Rust 不是。在本章节中,我们会探索一些被普遍认为是面向对象的特性和这些特性是如何体现在 Rust 语言习惯中的。接着会展示如何在 Rust 中实现面向对象设计模式,并讨论这么做与利用 Rust 自身的一些优势实现的方案相比有什么取舍。
Rust 中的面向对象
面向对象编程语言所共享的一些特性往往是对象、封装和继承。让我们看一下这每一个概念的含义以及 Rust 是否支持他们。
对象包含数据和行为
Object-oriented programs are made up of objects. An object packages both data and the procedures that operate on that data. The procedures are typically called methods or operations . 面向对象的程序是由对象组成的。一个 对象 包含数据和操作这些数据的过程。这些过程通常被称为 方法 或 操作 。 —— Design Patterns: Elements of Reusable Object-Oriented Software (被俗称为 The Gang of Four)
在这个定义下,Rust 是面向对象的:结构体和枚举包含数据而 impl
块提供了在结构体和枚举之上的方法。虽然带有方法的结构体和枚举并不被 称为 对象,但是他们提供了与对象相同的功能,参考 The Gang of Four 中对象的定义。
封装隐藏了实现细节
封装 (encapsulation ):
- 对象的实现细节不能被使用对象的代码获取到
- 唯一与对象交互的方式是通过对象提供的公有 API
- 使用对象的代码无法深入到对象内部并直接改变数据或者行为
- 封装使得改变和重构对象的内部时无需改变使用对象的代码
Rust 满足这个要求:在代码中不同的部分使用 pub
与否可以封装其实现细节。
Example:封装 AveragedCollection
的实现细节,将来可以轻松改变类似数据结构这些方面的内容。
例如,可以使用 HashSet<i32>
代替 Vec<i32>
作为 list
字段的类型。只要 add
、remove
和 average
公有函数的签名保持不变,使用 AveragedCollection
的代码就无需改变。
相反如果使得 list
为公有,就未必都会如此了: HashSet<i32>
和 Vec<i32>
使用不同的方法增加或移除项,所以如果要想直接修改 list
的话,外部的代码可能不得不做出修改。
// src/lib.rs
#[derive(Debug)]
pub struct AveragedCollection {
// 传入 Vec,自动计算其均值
// 删除、添加元素之后自动更新均值
// 如果把 list 字段公开,则用户直接可以修改值
// 然而 average 的值不会被自动更新
// 所以这两个值都是私有,需要访问时使用同名关联函数
list: Vec<i32>,
average: f64,
}
impl AveragedCollection {
pub fn new(v: Vec<i32>) -> Self {
let mut s = Self { list: v, average: 0. };
s.update_average();
s
}
pub fn add(&mut self, value: i32) {
self.list.push(value);
self.update_average();
}
pub fn remove(&mut self) -> Option<i32> {
let result = self.list.pop();
match result {
Some(value) => {
self.update_average();
Some(value)
}
None => None,
}
}
// 利用不可变引用来提供结构体内部的值,无法通过外部代码修改
pub fn list(&self) -> &Vec<i32> { &self.list }
pub fn average(&self) -> f64 { self.average }
fn update_average(&mut self) {
let total: i32 = self.list.iter().sum();
self.average = total as f64 / self.list.len() as f64;
}
}
使用:
// src/main.rs
use crate_full_name::oop::AveragedCollection;
fn main() {
let mut ac = AveragedCollection::new(vec![1, 3]);
println!("{:?}", ac);
println!("average: {}", ac.average());
ac.add(5);
println!("{:?}", ac);
println!("average: {}", AveragedCollection::average(&ac));
ac.remove();
// 不可通过外部外部代码修改结构体的字段值
// ac.list.pop();
println!("{:?}", ac);
println!("list: {:?}", ac.list());
// list 和 average 都是私有字段,无法直接访问
// println!("list: {:?}", ac.list);
// println!("average: {}", ac.average);
}
打印结果:
AveragedCollection { list: [1, 3], average: 2.0 }
average: 2
AveragedCollection { list: [1, 3, 5], average: 3.0 }
average: 3
AveragedCollection { list: [1, 3], average: 2.0 }
list: [1, 3]
继承:作为类型系统与代码共享
继承 (Inheritance )是一个很多编程语言都提供的机制,一个对象可以定义为继承另一个对象的定义,这使其可以获得父对象的数据和行为,而无需重新定义。
如果一个语言必须有继承才能被称为面向对象语言的话,那么 Rust 就不是面向对象的。Rust 无法定义一个结构体继承父结构体的成员和方法。
选择继承有两个主要的原因:
- 为了重用代码:一旦为一个类型实现了特定行为,继承可以对一个不同的类型重用这个实现。相反 Rust 代码可以使用默认 trait 方法实现来进行共享,也可以选择覆盖默认实现,这类似于子类覆盖从父类继承的方法实现。
- 与类型系统有关:表现为子类型可以用于父类型被使用的地方。这也被称为多态(polymorphism),这意味着如果多种对象共享特定的属性,则可以相互替代使用。Rust 通过泛型来对不同的可能类型进行抽象,并通过 trait bounds 对这些类型所必须提供的内容施加约束。
多态(Polymorphism)是一个有关可以用于多种类型的代码的更广泛的概念,而不限于继承。
Rust 选择使用 trait 对象而不是继承的原因:继承时常带有共享多于所需的代码的风险。子类不应总是共享其父类的所有特征,但是继承却始终如此。如此会使程序设计更为不灵活,并引入无意义的子类方法调用,或由于方法实际并不适用于子类而造成错误的可能性。某些语言还只允许子类继承一个父类,进一步限制了程序设计的灵活性。
trait 对象:为使用不同类型的值而设计
Rust 刻意不将结构体与枚举称为 “对象”,以便与其他语言中的对象相区别。
在结构体或枚举中,结构体字段中的数据和 impl
块中的行为是分开的,不同于其他语言中将数据和行为组合进一个称为对象的概念中。
trait 对象 (object) 将数据和行为两者相结合,从这种意义上说 则 其更类似其他语言中的对象。不过 trait 对象不同于传统的对象,因为不能向 trait 对象增加数据。trait 对象并不像其他语言中的对象那么通用:其(trait 对象)具体的作用是允许对通用行为进行抽象。
语法为:在具体的 trait 前加上 指针类型+dyn关键字。指针包括 &
、和智能指针。例子 &dyn OneTrait
、Box<dyn OneTrait>
。
几种使用多类型的方法
- 利用枚举类型:在固定的几种不同类型中选择
优点:编译时就能确定类型需要的大小,对于枚举体,我们知道其空间上限,运行效率高
缺点:需要列举出所有可能需要的类型组合
enum SpreadsheetCell {
Int(i32),
Float(f64),
Text(String),
}
let row = vec![ // Vec 的所有元素都是 SpreadsheetCell 类型
SpreadsheetCell::Int(3),
SpreadsheetCell::Text(String::from("blue")),
SpreadsheetCell::Float(10.12),
];
vector 只能存储同种类型元素的局限,可以定义 SpreadsheetCell
枚举来储存整型,浮点型和文本成员。这意味着可以在每个单元中储存不同类型的数据,并仍能拥有一个代表一排单元的 vector。
- 泛型+trait bound:在 trait bound 内使用一种具体类型
优点:其定义会在编译时采用具体类型进行单态化,适合只需要同质(相同类型)集合;比枚举类型更加灵活。
缺点:一次只能替代一个具体类型
pub trait Draw {
fn draw(&self);
}
pub struct Screen<T: Draw> {
// Vec 里面是泛型 T,其元素是带有 Draw trait 的任何一种类型
// 由于泛型编译时的单态化,一旦这个类型固定下来,所有元素只能是这种具体类型
pub components: Vec<T>,
}
impl<T> Screen<T>
where T: Draw {
pub fn run(&self) {
for component in self.components.iter() {
component.draw();
}
}
}
- trait 对象 (object):允许在运行时替代多种具体类型
trait 对象指向一个实现了我们指定 trait 的类型的实例,以及一个用于在运行时查找该类型的trait方法的表。我们通过指定某种指针来创建 trait 对象,例如&
引用或Box<T>
智能指针,还有dyn
keyword, 以及指定相关的 trait。
优点:我们可以使用 trait 对象代替泛型或具体类型,无需在编译时就知晓所有可能的类型。
缺点:运行时性能会受到影响
// gui crate: src/lib.rs
pub trait Draw {
fn draw(&self);
}
pub struct Screen {
// 可以存放实现 Draw trait 的任何类型
// Vec 的所有元素是 Box 类型,Box 是一个智能指针,把内部的数据放在堆上,Box 里面可以是任何类型
// 因此 Box<Button>、Box<TextField> 等多种类型可以同时放入 Vec
// 需要注意的是,trait 对象是编译时不能完全确定下来的类型,所以需要 dyn 关键字来表明
pub components: Vec<Box<dyn Draw>>,
}
impl Screen {
pub fn run(&self) {
for component in self.components.iter() {
component.draw();
}
}
}
pub struct Button {
pub width: u32,
pub height: u32,
pub label: String,
}
impl Draw for Button {
fn draw(&self) {
// 实际绘制按钮的代码
}
}
pub struct TextField {
pub width: u32,
pub height: u32,
pub content: String,
}
impl Draw for TextField {
fn draw(&self) {
// 实际绘制按钮的代码
}
}
如果 crate 的使用者决定实现一个包含 width
、height
和 options
字段的结构体 SelectBox
,并且也为其实现了 Draw
trait:
// crate user: src/lib.rs
use gui::Draw;
struct SelectBox {
width: u32,
height: u32,
options: Vec<String>,
}
impl Draw for SelectBox {
fn draw(&self) {
// code to actually draw a select box
}
}
使用者就可以他们的 main
函数中创建一个 Screen
实例。至此可以通过将 SelectBox
和 Button
放入 Box<T>
转变为 trait 对象来增加组件。接着可以调用 Screen
的 run
方法,它会调用每个组件的 draw
方法。
// crate user: src/main.rs
use gui::{Screen, Button};
fn main() {
let screen = Screen {
components: vec![
Box::new(SelectBox {
width: 75,
height: 10,
options: vec![
String::from("Yes"),
String::from("Maybe"),
String::from("No")
],
}),
Box::new(Button {
width: 50,
height: 10,
label: String::from("OK"),
}),
],
};
screen.run();
}
- 当编写 gui crate 的时候,我们不知道何人会在何时增加
SelectBox
类型,不过Screen
的实现能够操作并绘制这个新类型,因为SelectBox
实现了Draw
trait,这意味着它实现了draw
方法。这个概念 —— 只关心值所反映的信息而不是其具体类型 —— 类似于动态类型语言中称为 鸭子类型 (duck typing )的概念:如果它走起来像一只鸭子,叫起来像一只鸭子,那么它就是一只鸭子! - 实现中(这里是使用 gui crate 的时候),
run
并不需要知道各个组件的具体类型是什么。它并不检查组件是Button
或者SelectBox
的实例。通过指定Box<dyn Draw>
作为components
vector 中值的类型,我们就定义了Screen
为需要可以在其上调用draw
方法的值。static & dynamic dispatch
静态/动态分派拓展阅读:https://www.jianshu.com/p/e0659093eaac
分派 (dispatch):处理方法调用的过程。一个方法会在运行时被调用,是因为编译器有一个计算机制,用来选择正确的方法,然后通过传递参数来唤起它。这个机制通常被称为 dispatch。
静态分派 (static dispatch):在编译期确定需要调用的方法,在运行期进行调用。特点如下:
- 标准做法:在编译器确定使用 static dispatch 后,会在生成的可执行文件内 直接指定包含了方法实现内存地址的指针;运行时 直接通过指针调用特定的方法。
- 优化做法:内联 (inline):编译期从指定被调用的方法指针,改为将方法的实现平铺在调用方的可执行文件内。
- inline 可以人为声明,也可以通过编译器优化来实现
- 和宏展开类似,区别在于:内联发生在编译期,并且不会改变源文件;宏展开是在编译前就完成的,会改变源码本身。之后再对此进行编译
- 内联可以用于消减方法被调用的时间,非常适用于会被频繁调用的方法。如果方法本身很小的话,可以降低内存上的消耗。内联还为进一步的编译优化提供了基础。
- 有些内联可以通过很小的内存消耗来提升运行速度。但是无节制的内联,也可能会降低速度,因为内联的代码需要大量的CPU 缓存,并且也会消耗内存空间
动态分派 (dynamic dispatch):在运行期选择调用方法的实现的流程,目的是为了支持在编译期无法确定最终最合适的实现的操作。
- 比静态分派带来更多的性能损耗。优点在于编程时灵活。
- single dispatch:通过对象类型去选择调用方法的模式。如
dividend.divide(divisor)
调用 divide 方法。 - multiple dispatch:根据方法名结合方法的参数,一起来判断需要执行的方法。
- 实现方式:虚函数表
当对泛型使用 trait bound 时编译器所进行单态化处理:编译器为每一个被泛型类型参数代替的具体类型生成了非泛型的函数和方法实现。单态化所产生的代码进行 静态分发 (static dispatch )。静态分发发生于编译器在编译时就知晓调用了什么方法的时候。
动态分发 (dynamic dispatch ):编译器在编译时无法知晓调用了什么方法。在动态分发的情况下,编译器会生成在运行时确定调用了什么方法的代码。
- 当使用 trait 对象时,Rust 必须使用动态分发。编译器无法知晓所有可能用于 trait 对象代码的类型,所以它也不知道应该调用哪个类型的哪个方法实现。因此 Rust 在运行时使用 trait 对象中的指针来知晓需要调用哪个方法。
动态分发阻止编译器有选择的内联方法代码,这会相应的禁用一些优化。
trait 对象要求对象安全
只有 对象安全 (object safe )的 trait 才可以组成 trait 对象。如果一个 trait 中所有的方法有如下属性时,则该 trait 是对象安全的:
返回值类型不为
Self
- 方法没有任何泛型类型参数
Self
关键字是我们要实现 trait 或方法的类型的别名。对象安全对于 trait 对象是必须的,因为一旦有了 trait 对象,就不再知晓实现该 trait 的具体类型是什么了。如果 trait 方法返回具体的 Self
类型,但是 trait 对象忘记了其真正的类型,那么方法不可能使用已经忘却的原始具体类型。
同理对于泛型类型参数来说,当使用 trait 时其会放入具体的类型参数:此具体类型变成了实现该 trait 的类型的一部分。当使用 trait 对象时其具体类型被抹去了,故无从得知放入泛型参数类型的类型是什么。
trait object 不安全的例子:标准库中的 Clone
trait。
pub trait Clone {
fn clone(&self) -> Self;
}
如果违反 object safe 规则:
pub struct Screen {
pub components: Vec<Box<dyn Clone>>,
}
编译器会不通过:
the trait `std::clone::Clone` cannot be made into an object
oo design pattern
面向对象设计模式:状态模式 (state pattern )
- 关键在于一个值有某些内部状态,体现为一系列的 状态对象 ,同时值的行为随着其内部状态而改变
- 在 Rust 中使用结构体和 trait 让状态对象共享功能
- 每一个状态对象负责其自身的行为,以及该状态何时应当转移至另一个状态
- 状态模式的优点:灵活性、拓展性。
- 当程序的业务需求改变时,无需改变值持有状态或者使用值的代码。我们只需更新某个状态对象中的代码来改变其规则,或者是增加更多的状态对象。
- 根据所处的状态来封装不同类型的行为,从而实现各个状态独立的功能
- 状态共有的功能则通过 trait 对象实现
例子:发布博文的工作流
- 博文从空白的草案开始。
- 一旦草案完成,请求审核博文。
- 一旦博文过审,它将被发表。
- 只有被发表的博文的内容会被打印,这样就不会意外打印出没有被审核的博文的文本。
任何其他对博文的修改尝试都是没有作用的。例如,如果尝试在请求审核之前通过一个草案博文,博文应该保持未发布的状态。
这里包括 三种状态:草稿、待审核、发布成功。每种状态可能会有其单独的信息,因而把它们定义成结构体;定义这些状态对象联系的纽带 State trait 和共有的方法。
状态对象 | Draft | PendingReview | Published |
---|---|---|---|
状态转移 | Post::new() |
Post::request_review() |
Post::approve() |
// crate name: blog
// src/main.rs
use blog::Post;
fn main() {
let mut post = Post::new();
post.add_text("I ate a salad for lunch today");
assert_eq!("", post.content());
post.request_review();
assert_eq!("", post.content());
post.approve();
assert_eq!("I ate a salad for lunch today", post.content());
}
// crate name: blog
// src/lib.rs
#![allow(unused)]
pub struct Post {
// state 和 content 都是私有的
state: Option<Box<dyn State>>,
// 调用同名方法 .content() 来获取不同状态下的 content
// 具体来说,Draft、PendingReview 状态时外部调用 .content() 为 空串
// Approve 状态下 .content() 才会返回 Post.content
content: String,
}
impl Post {
// 无论何时新建一个 Post 实例,它都会从 Draft 状态开始
// 因为 Post 的 state 字段是私有的,也就无法创建任何其他状态的 Post 了
pub fn new() -> Post {
Post { state: Some(Box::new(Draft {})), content: String::new() }
}
// add_text 方法完全不与 state 状态交互。因为它的行为并不依赖博文所处的状态
pub fn add_text(&mut self, text: &str) { self.content.push_str(text); }
pub fn content(&self) -> &str {
// as_ref 获取 Option 中值的引用而不是其所有权,它返回 Option<&Box<State>>
// 如果不调用 as_ref,会得到错误:不能将 state 移动出借用的 &self 函数参数
// 调用 unwrap 方法,这里我们知道它永远也不会 panic,因为 Post
// 的所有方法都确保在他们返回时 state 会有一个 Some 值
self.state.as_ref().unwrap().content(self)
}
// Post 的 request_review 方法无论 state 是何值都是一样的
pub fn request_review(&mut self) {
// Option.take 方法会把 Option 内的值取出,把默认值 None 替换成原变量值
if let Some(s) = self.state.take() {
// 这使得我们将 state 值移动出 Post 而不是借用它
// 接着将博文的 state 值设置为这个操作的结果:Some(PendingReview)
self.state = Some(s.request_review())
}
}
pub fn approve(&mut self) {
if let Some(s) = self.state.take() {
self.state = Some(s.approve())
}
}
}
// State trait 定义了所有不同状态的博文所共享的行为
// 同时 Draft、PendingReview 和 Published 状态都会实现 State 状态
trait State {
// self: Box<Self> 这个语法意味着这个方法调用只对这个类型的 Box 有效
// 这个语法获取了 Box<Self> 的所有权,使老状态无效化
// 以便 Post 的状态值可以将自身转换为新状态
// 待审核状态
fn request_review(self: Box<Self>) -> Box<dyn State>;
// 发布状态
fn approve(self: Box<Self>) -> Box<dyn State>;
// content 方法的默认实现来返回一个空字符串 slice
// 这意味着无需为 Draft 和 PendingReview 结构体实现 content 了
// 返回的引用的生命周期与 post 参数相关
fn content<'a>(&self, post: &'a Post) -> &'a str { "" }
}
struct Draft {}
impl State for Draft {
fn request_review(self: Box<Self>) -> Box<dyn State> { Box::new(PendingReview {}) }
fn approve(self: Box<Self>) -> Box<dyn State> { Box::new(Published {}) }
}
struct PendingReview {}
impl State for PendingReview {
fn request_review(self: Box<Self>) -> Box<dyn State> { self }
fn approve(self: Box<Self>) -> Box<dyn State> { Box::new(Published {}) }
}
struct Published {}
impl State for Published {
fn request_review(self: Box<Self>) -> Box<dyn State> { self }
fn approve(self: Box<Self>) -> Box<dyn State> { self }
fn content<'a>(&self, post: &'a Post) -> &'a str { &post.content }
}
通过面向对象的状态模式,我们能根据博文所处的状态来封装不同类型的行为:Post
的方法并不知道这些不同类型的行为。通过这种组织代码的方式,要找到所有已发布博文的不同行为只需查看一处代码:Published
的State
trait 的实现。
如果要创建一个不使用状态模式的替代实现,则可能会在 Post
的方法中,或者甚至于在 main
代码中用到 match
语句,来检查博文状态并在这里改变其行为。这意味着需要查看很多位置来理解处于发布状态的博文的所有逻辑!这在增加更多状态时会变得更糟:每一个 match
语句都会需要另一个分支。
对于状态模式来说,Post
的方法和使用 Post
的位置无需 match
语句,同时增加新状态只涉及到增加一个新 struct
和为其实现 trait 的方法。
这个实现易于扩展增加更多功能。为了体会使用此模式维护代码的简洁性,请尝试如下一些建议:
- 增加
reject
方法将博文的状态从PendingReview
变回Draft
- 在将状态变为
Published
之前需要两次approve
调用 - 只允许博文处于
Draft
状态时增加文本内容。提示:让状态对象负责内容可能发生什么改变,但不负责修改Post
。
当前例子使用状态模式的缺点:
- 是因为状态实现了状态之间的转换,一些状态会相互联系。如果在
PendingReview
和Published
之间增加另一个状态,比如Scheduled
,则不得不修改PendingReview
中的代码来转移到Scheduled
。如果PendingReview
无需因为新增的状态而改变就更好了,不过这意味着切换到另一种设计模式。 - 存在一些重复的逻辑。
Post
中request_review
和approve
这两个类似的实现都调用了state
字段中Option
值的同一方法,并在结果中为state
字段设置了新值。如果Post
中的很多方法都遵循这个模式,我们可能会考虑定义一个宏来消除重复。#todo:用宏改写这个例子#
当然,如果不使用 Option
改变 Post.state
的值也是可以的,关键在于 state 值的改变发生在 lib crate 中,而无需使用者更改。
所以可以使用 RefCell
达到内部可变性,从而替代了 Option.take
做的事情。当然,从效率上说,肯定 Option
的方式更高。
// crate name: blog
// src/lib.rs
use std::cell::RefCell;
pub struct Post {
state: RefCell<Box<dyn State>>,
content: String,
}
impl Post {
pub fn new() -> Post {
Post { state: RefCell::new(Box::new(DraftPost {})), content: String::new() }
}
pub fn add_text(&mut self, content: &str) { self.content.push_str(content); }
pub fn content(&self) -> &str { self.state.borrow().content(self) }
pub fn request_review(&mut self) {
let new_state = self.state.borrow().request_review();
let new_state = RefCell::new(new_state);
self.state.swap(&new_state);
}
pub fn approve(&mut self) {
let new_state = self.state.borrow().approve();
let new_state = RefCell::new(new_state);
self.state.swap(&new_state);
}
}
trait State {
fn content<'a>(&self, _post: &'a Post) -> &'a str { "" }
fn request_review(&self) -> Box<dyn State>;
fn approve(&self) -> Box<dyn State>;
}
struct DraftPost {}
struct PendingReviewPost {}
struct PublishedPost {}
impl State for DraftPost {
fn request_review(&self) -> Box<dyn State> { Box::new(PendingReviewPost {}) }
fn approve(&self) -> Box<dyn State> { Box::new(Self {}) }
}
impl State for PendingReviewPost {
fn request_review(&self) -> Box<dyn State> { Box::new(Self {}) }
fn approve(&self) -> Box<dyn State> { Box::new(PublishedPost {}) }
}
impl State for PublishedPost {
fn content<'a>(&self, post: &'a Post) -> &'a str { post.content.as_str() }
fn request_review(&self) -> Box<dyn State> { Box::new(Self {}) }
fn approve(&self) -> Box<dyn State> { Box::new(Self {}) }
}
完全按照面向对象语言的定义实现这个模式并没有尽可能地利用 Rust 的优势。这个例子可以利用不同的类型来实现状态转移:
// src/main.rs
use blog::Post;
fn main() {
let mut post = Post::new(); // mut post: DraftPost
post.add_text("I ate a salad for lunch today");
let post = post.request_review(); // post: PendingReviewPost
let post = post.approve(); // post: Post
assert_eq!("I ate a salad for lunch today", post.content());
}
// `blog` crate: src/lib.rs
pub struct Post {
content: String,
}
pub struct DraftPost {
content: String,
}
impl Post {
pub fn new() -> DraftPost { DraftPost { content: String::new() } }
pub fn content(&self) -> &str { &self.content }
}
impl DraftPost {
pub fn add_text(&mut self, text: &str) { self.content.push_str(text); }
pub fn request_review(self) -> PendingReviewPost {
PendingReviewPost { content: self.content }
}
}
pub struct PendingReviewPost {
content: String,
}
impl PendingReviewPost {
pub fn approve(self) -> Post { Post { content: self.content } }
}
“改进” 版:
使用 trait object 和面向对象编程范式:适合逻辑重合度高的场景,但它并不总是最好的解决方案,因为 Rust 拥有像所有权这样的面向对象语言所没有的功能,而且动态分配会有少量的运行时性能损耗。
// `blog` crate: src/lib.rs
pub struct Post {
state: Option<Box<dyn State>>,
state_count: u32,
content: String,
editable: bool,
}
impl Post {
pub fn new() -> Post {
Post { state: Some(Box::new(Draft {})),
content: String::new(),
state_count: 1,
editable: true, }
}
pub fn add_text(&mut self, text: &str) {
if self.editable {
self.content.push_str(text);
} else {
println!("Contents are not editable and not changed.");
}
}
pub fn content(&self) -> &str { self.state.as_ref().unwrap().content(self) }
pub fn request_review(&mut self) {
if let Some(s) = self.state.take() {
self.state_count += 1;
self.editable = false;
self.state = Some(s.request_review())
}
}
pub fn approve(&mut self) {
if self.state_count == 3 {
if let Some(s) = self.state.take() {
self.state = Some(s.approve())
}
}
}
pub fn reject(&mut self) {
if let Some(s) = self.state.take() {
self.state_count = 0;
self.editable = true;
self.state = Some(s.reject())
}
}
}
trait State {
fn request_review(self: Box<Self>) -> Box<dyn State>;
fn approve(self: Box<Self>) -> Box<dyn State>;
fn content<'a>(&self, post: &'a Post) -> &'a str { "" }
fn reject(self: Box<Self>) -> Box<dyn State> { Box::new(Draft {}) }
}
struct Draft {}
impl State for Draft {
fn request_review(self: Box<Self>) -> Box<dyn State> { Box::new(PendingReview {}) }
fn approve(self: Box<Self>) -> Box<dyn State> { Box::new(Published {}) }
}
struct PendingReview {}
impl State for PendingReview {
fn request_review(self: Box<Self>) -> Box<dyn State> { self }
fn approve(self: Box<Self>) -> Box<dyn State> { Box::new(Published {}) }
}
struct Published {}
impl State for Published {
fn request_review(self: Box<Self>) -> Box<dyn State> { self }
fn approve(self: Box<Self>) -> Box<dyn State> { self }
fn content<'a>(&self, post: &'a Post) -> &'a str { &post.content }
}
// src/main.rs
use blog::Post;
fn main() {
let mut post = Post::new();
post.add_text("I ate a salad for lunch today");
assert_eq!("", post.content());
post.request_review();
assert_eq!("", post.content());
post.reject();
assert_eq!("", post.content());
// reject 让 state 回到 Draft,因而可以编辑
post.add_text(" lol");
post.request_review();
assert_eq!("", post.content());
// 必须两次调用 request_review 才能进入到 published state
post.approve();
assert_eq!("", post.content());
post.request_review();
assert_eq!("", post.content());
post.request_review();
assert_eq!("", post.content());
// 这里实际没有编辑,会打印 `Contents are not editable and not changed.`
post.add_text(" foo");
post.approve();
assert_eq!("I ate a salad for lunch today lol", post.content());
}
使用类型系统:显然思路也清晰,而且代码量少、拓展性同样高;充分发挥 Rust 的语言特性,比如这里的所有权、数据类型、编译时检查。
// `blog` crate: src/lib.rs
pub struct Post {
content: String,
}
pub struct DraftPost {
content: String,
}
impl Post {
pub fn new() -> DraftPost { DraftPost { content: String::new() } }
pub fn content(&self) -> &str { &self.content }
}
impl DraftPost {
pub fn add_text(&mut self, text: &str) { self.content.push_str(text); }
pub fn request_review(self) -> PendingReviewPost {
PendingReviewPost { content: self.content }
}
}
pub struct PendingReviewPost {
content: String,
}
impl PendingReviewPost {
pub fn reject(self) -> DraftPost { DraftPost { content: self.content } }
pub fn request_review(self) -> ReviewApprovedPost {
ReviewApprovedPost { content: self.content }
}
}
pub struct ReviewApprovedPost {
content: String,
}
impl ReviewApprovedPost {
pub fn approve(self) -> Post { Post { content: self.content } }
}
// src/main.rs
use rust_book_coding_in_practice::blog_use_type_extended::Post;
fn main() {
let mut post = Post::new();
post.add_text("I ate a salad for lunch today");
let post = post.request_review();
// reject 将退回到 DraftPost,并且只能在 DraftPost 类型中编辑
let mut post = post.reject();
post.add_text(" lol");
// 两次调用 request_review 的实例类型不一样
// DraftPost -> PendingReviewPost
let post = post.request_review();
// PendingReviewPost -> ReviewApprovedPost
let post = post.request_review();
// ReviewApprovedPost -> Post
let post = post.approve();
assert_eq!("I ate a salad for lunch today lol", post.content());
}