对照OOP
浅谈【类型状态】设计模式
类型状态·设计模式Type State Pattern
也被称作“泛型·即是·类的类型(约束)Generic as Type Class (Constraint)
”。它是基于Rust
独有语言特性
单态化
monomorphization
move
赋值语义
的新颖设计模式。其中,【move
赋值语义】为Rust
所独有的原因是
一方面,
GC
类计算机语言已经将内存托管给vm
,所以他们就没再搞出这类复杂的内存管理概念,和增加开发者的心智负担。另一方面,
Cpp
的move
赋值语义仅只是对历史包袱的【妥协方案】。这体现在所以,和
Rust
相比,Cpp
的move
赋值语义至多就是一个“弟弟”。其功能相当于Rust
标准库提供的std::mem::take(&T) -> T
内存操作 — 使用【类型·默认值】置换出【引用】内存位置上的值;同时,保留·原变量·的【所有权】不被消耗掉和可以被接着使用。Cpp
的move
语义是: 用空指针nullptr
换走原变量的值;但,原变量依旧可访问。这哪里是move
,分明是swap
呀!Rust
的move
语义是:拿走原变量的值;同时,作废原变量。这个操作也被称为“消耗consuming
”。
此外,
move
也不是Cpp
变量赋值的默认语义。相反 ,开发者得显示地编码std::move(ptr)
函数调用和将lvalue
转换为rvalue
。Cpp
的std::move(ptr)
函数调用是【零·运行时·成本】的。在编译之后,编译器会将其从机器码内扣掉。其“辅助线”般的功能有些类似于Rust
中的std::marker::PhantomData<T>
。
名词解释
为了避免后续文章内容过于啰嗦,首先定义五个词条,包括:
泛型类型
泛型类型参数
泛型类型形参
泛型类型实参
泛型类型参数限定条件
一图抵千词,大家来看下图吧:
基本概念
【类型状态·模式】的初衷是:将【运行时】对象状态的(动态)信息·编码入·【编译时】对象类型的(静态)定义里。进而,借助现成且完备的Rust
【类型系统】,在【编译】过程中,确保:
处于不同状态的(泛型类型)实例·拥有不一样的(【成员方法】+【关联函数】+【字段】)集合。
(泛型类型)实例·仅能在毗邻的状态之间进行“状态·过渡”,而不能“跳变”。
排查出·跨状态的成员方法调用。比如,
A
状态的实例调用了仅在B
状态才有效的成员方法。 而不是,让这类错误潜伏着和等【测试覆盖】或抛出【运行时·异常】。
以【订单系统】为例,【编译器】就能筛查出代码里
对【无效订单】实例的【发货】成员方法调用
对【出库订单】实例的【完成】成员方法调用 — 还未经历【发货】与【收款】两个状态
相对于传统的OOP
程序,Rust
【类型状态】设计模式将【对象·状态】的【运行时】检查前置于【编译环节】。进而带来的好处包括但不限于:
将【运行时】程序崩溃“无害化”为【编译时】错误。
就开发者而言,这意味着更短的【思考
+
试错】反馈回路。就应用程序而言,这意味着更高的性能,更健壮的可靠性,和更重的应用程序大小 — 【单态化】的本质就是以空间换时间。
允许
IDE
提供更有价值的代码提示。即,仅智能地列出对当前状态实例有效的【成员方法】,而不是罗列全部成员方法。比如,当开发者“点”一个【无效订单】实例时,IDE
就不应该提示出【发货】成员方法。这才是对开发者最实在的帮助。
代码套路
从操作细节来说,为了采用【类型状态·设计模式】,我们需要:
将每个【状态】分别映射为独立的【结构体】(比如,
struct State1
)。和在结构体内,定义【状态】独有的:字段。(见伪码#1
注释)而不是,使用一个【枚举类】
enum State {...}
笼统地描述所有【状态】后文称这类【结构体】为【状态·类型】。
以【泛型·类型】
+
【泛型·类型·形参】的结构体定义(比如,struct Type1<S1>
),抽象所有【状态】共有的:字段。(见伪码#2
注释)以【泛型·类型】
+
【泛型·类型·形参】的实现块(比如,impl<S1> Type1<S1>
),抽象所有【状态】共有的:成员方法,关联函数,关联常量,和关联类型。(见伪码#3
注释)以【泛型·类型】
+
【泛型·类型·实参】的实现块(比如,impl Type1<State1>
),定制每个【状态】独有的:成员方法,关联函数,关联常量,和关联类型。(见伪码#4
注释)
-
/// #1 【状态·类型】
-
struct State1 {
-
private_field1: String
// 定义【状态】独有【字段】
-
}
-
struct State2 {
-
private_field2: String
// 定义【状态】独有【字段】
-
}
-
/// #2 【泛型·类型】+【泛型·类型·形参】
-
struct Type1<S1> {
// <- 被参数化的【状态·类型】既作为【泛型·类型·参数】,
-
state: S1,
// <- 也作为【状态·字段】的字段类型
-
com_field0: String
// 抽象全部【状态】共有【字段】
-
}
-
/// #3 【泛型·类型】+【泛型·类型·形参】
-
impl<S1> Type1<S1> {
// 抽象全部【状态】共有的【成员方法】
-
fn com_function0(&self) -> String {
-
"全状态可调用".to_string()
-
}
-
}
-
/// #4 【泛型·类型】+【泛型·类型·实参】
-
impl Type1<State1> {
// 定制【状态】`State1`独有【成员方法】
-
fn private_function1(&self) -> String {
-
"仅 State1 状态可调用".to_string()
-
}
-
}
-
impl Type1<State2> {
// 定制【状态】`State2`独有【成员方法】
-
fn private_function2(&self) -> String {
-
"仅 State2 状态可调用".to_string()
-
}
-
}
承上段代码,在【泛型·类型】struct Type1<S1>
中,被参数化的【状态·类型】S1
既作为【泛型·类型·参数】也作为【状态·字段】state
的字段类型(这是由Generic Struct
定义要求的 — 在结构体定义中,被声明的泛型参数必须被使用)。
-
// 继续前面的代码
-
let type1_state1 = Type1 {
-
com_field0:
"对所有状态都看得到的,共用字段值".to_string(),
-
// 锚定 type1_state1 实例处于 State1 状态
-
state: State1 {
-
private_field1:
"状态1的私有字段值。对其它任何状态都不可见".to_string()
-
}
-
};
-
// 即便对 Type1<State2> 实例,此【成员方法】调用也是成立的。
-
dbg!(type1_state1.com_function0());
-
// 对 Type1<State2> 实例,此会报编译错误。
-
dbg!(type1_state1.private_function1());
-
// 对【状态】独有【字段】的取值语句则有些“啰嗦”了。
-
dbg!(&type1_state1.state.private_field1[..]);
承上段代码,除了【状态】State1
的独有【字段】private_field1
需要隔着一层【状态·字段】state
取值(如,type1_state1.state.private_field1
),所有其它的【项】都能从type1_state1
实例上直接“点出来”(如,type1_state1.private_function1()
)。
虽然【状态】独有【字段】的取值语句有些冗长,但语法是“死”的,可人是“活”的呀!再额外封装一个【状态】独有getter
【成员方法】即可简化字段取值操作。
-
// 继续前面的代码
-
impl Type1<State1> {
-
// 既可缩短【状态】独有字段的取值路径,
-
// 也可抹掉 <Type1>.state 与 <State1>.private_field1 字段的`pub`可见性修饰符。
-
fn private_field1(&self) -> &str {
-
&self.state.private_field1[..]
-
}
-
}
-
// 取值语句是不是精简多了?此外,该成员方法对 Type1<State2> 类型实例不可见。
-
dbg!(type1_state1.private_field1());
至此,一个完整的【例程】往这里看。
代码结构·示意图
文档注释小技巧
将描述【状态】含义的doc comments
放在(【泛型·类型】+【泛型·类型·实参】)实现块impl Type1<State1>
的上端,而不是在【状态·类型】结构体定义struct State1
之上。那么, rustdoc
便会把全部【状态】的doc comments
文案都收拢于一个html
文档页内(即,【泛型·类型】struct Type1<S1>
文档页内里),而不是分散于多个html
页。这给“下游”开发者提供了更友好的文档阅读连贯性。
-
/// 将描述【状态】的【文档注释】加在这里,
-
impl Type1<State1> {...}
-
/// 而不是加在这里。借助`intra-doc link`注释指令:[`Type1<State1>`](struct@crate::Type1#impl-Type1<State1>)
-
/// 你可以直接链接到上面`impl Type1<State1> {...}`的位置。
-
struct State1 {...}
借助intra-doc link
注释指令[
`Type1<State1>
`](struct@crate::Type1#impl-Type1<State1>)
,从【状态·类型】结构体定义struct State1
向(【泛型·类型】+【泛型·类型·实参】)实现块impl Type1<State1>
做文档链接,可以避免文档注释的大量重复。
对照OOP
概念
OOP
继承
Method
调用安全
由于Rust
【单态化】允许【成员方法 / 关联函数】仅对特定的(【泛型·类型】+【泛型·类型·实参】)组合可见(比如,Type1<State1>
),所以遵从【类型·状态】设计模式的Rust
代码能够保证调用安全。即,凡是被【编译器】审核通过的【成员方法】调用,即便到了【运行时】,其也是语义/状态正确的。而,不需要开发者在【成员方法】起始位置附加额外的“防御性”判断,以禁止其运行于不匹配的状态。
在OOP
程序中,自觉地添加“防御性”判断是资深程序员的基本素养。进而,避免【成员方法】被错误地运行于不匹配状态,执行未定义行为,和输出逻辑错误结果。于是,虽然不能(如Rust
单态化)阻止错误成员方法调用的出现,但至少能(凭“防御性”代码)拒绝错误调用的执行 — 就是成本有点高,得以程序崩溃为代价。还好啦!至少坚守了底线。下面就是OOP
程序的反面例子。
-
/// 警告:不推荐这么写。这仅只是一个反面例子。
-
///
-
/// 假设一共有三个状态
-
enum State {
-
State1,
-
State2,
-
StateN
-
}
-
struct Type1 {
-
// 【枚举类·字段类型】笼统地概括了所有可能的【状态】
-
// 或者讲,所有的【状态】都是同一个类型。
-
state: State
-
}
-
impl Type1 {
-
// 根据设计,该成员方法仅只对`State1`状态的实例有效。
-
// 没有了【类型·状态】设计模式的赋能,`operate1()`成员方法便保证不了“调用安全”。
-
fn operate1(&mut self) {
-
// 运行时【防御性·判断】造成了
-
match &self.state {
// (1)重复的代码
-
State::State1 => {
-
// (2)更深的缩进
-
},
-
_ => {
// (3)潜在的崩溃点
-
panic!(
"我不能工作于 State2 与 State3 状态");
-
// 若能在 State2 与 State3 状态就点不到`operate1()`成员方法,
-
// 那就完美了。
-
}
-
};
-
}
-
}
与type state
模式的程序相比,此处的OOP
代码一下子就少了fluent
的感觉了。试想每个成员方法都如operate1()
这般臃肿,那将是多么令人烦躁的困境。
OOP
泛型
与Rust
相比 ,cpp/java
【泛型·类型】的“形状”(即,成员方法+
字段·的集合)永远是相同的,无论【泛型·类型·形参】被实际代入什么【具体类型】。这是因为
Rust
— 在【编译】语法分析阶段,借助于AST
,安全地生成新类型定义(单态化)。这不仅仅是代换入【泛型·类型·实参】这么初级。相反,每对(【泛型·类型】+
【泛型·类型·实参】)组合都是拥有新成员方法(和关联函数)的新类型。Cpp
— 在【编译】词法分析阶段,以“字符串替换”方式,将模板内的“占位符”安全地·调换为·具体“类型名”。这既没有对旧类型“形状”的裁剪,也没有对新类型的定义。Java
— 在【运行时】,将·具体值·代入Class Object
的泛型参数。无从谈起,新建或改变【类型定义】,因为Java
又不是动态语言。
所以,在这里再次重申我的观点:请不要吐槽
rustc
编译时间长了。你看它在【编译】期间明显地完成了更多得多的工作。这怎么可能有闪电般的编译速度呢?如果你的项目与团队对程序的编译延时有着非常苛刻的要求,没准你们需要“换一个脚本语言技术栈”。
OOP
状态字段
在仅OOP
的结构体定义中,【状态·字段】被设计为一个【枚举类】enum State {State1, State2, StateN}
和以一个类型笼统地描述所有【状态】,所以
不再需要【泛型·类型·参数】
S1
了。上例中的Type1
结构体也不是【泛型·类型】,而是普通结构体struct Type1
了。-
/// 【枚举类】笼统地概括了所有可能的【状态】
-
/// 或者讲,所有的【状态】都是同一个类型。
-
enum State {
-
State1,
-
State2,
-
StateN
-
}
-
/// 不再是【泛型类型】了
-
struct Type1 {
-
com_field0: String,
-
state: State // 状态字段
-
}
-
impl Type1 {
-
fn operate1(&mut self) {
-
// 1. 防御性·判断
-
// 2. 真正的业务逻辑代码
-
}
-
}
-
rustc
不会凭借【单态化】与【泛型·类型·实参】生成新类型了。不再保证
Method
调用安全,因为每个状态的结构体实例都能“点”出全部的【成员方法】,而不论被“点”出的成员方法与当前状态是否匹配。仅仅修改【状态·字段】的值,即可实现【状态·过渡】。而
Rust
类型状态设计模式却要求【状态·过渡】是“建新,弃旧;move
数据”的过程(详细见下文)。状态字段也不再是零抽象成本了
总之,Rust
类型状态设计模式与OOP
仅有一分相似却带九分不同:OOP
是·运行时·多态,而Type State pattern
是·编译时·多态。
状态·过渡
迥异于OOP
程序直接修改【状态·字段】的值self.state = State::State2;
,Rust
【类型·状态】设计模式则要求:
构造【新】状态的【新】实例(见伪码
#1
注释)消费掉【旧】状态的【旧】实例(见伪码
#2
注释)。进而,将旧状态的字段值com_field0
按值传递给新状态实例。
其背后的逻辑是:
Type1<State1>
与Type1<State2>
是两个不同的类型。从
State1
至State2
的状态过渡就是从Type1<State1>
至Type1<State2>
类型转换 — 更Rustacean
的表述就是impl From<Type1<State1>> for Type1<State2> {...}
。OOP
才用一个枚举类enum State
“笼统地”概括所有【状态】。然后,修改Type1.state
状态字段值·实现·状态过渡。
-
// 状态共有·实现块
-
impl<S1> Type1<S1> {
-
// 【状态·过渡】泛型函数 — 对所有状态都可见
-
// #2 消耗型成员方法 - 消费掉【旧】状态的【旧】实例
-
fn state_transition<NextState>(self, new_state: NextState) -> Type1<NextState> {
-
// #1 构造【新】状态的【新】实例
-
Type1 {
-
state: new_state,
// 代入下一个状态的【泛型·类型·实参】
-
com_field0: self.com_field0
// 同时“移入”状态共有的【字段值】。
-
// 注意:这里不能使用`..self`解构语法是因为`Type1<State1>`
-
// 与`Type1<State1>`的类型不同
-
}
-
// self.state = new_state; 会导致编译失败,因为类型不匹配。
-
}
-
}
至此,一个完整的【例程】往这里看。
在文章开篇就强调过:“【类型·状态】设计模式能够在【编译时】就筛查出无关【状态】之间的错误跳变”。为了具备这个能力,仅需对上面【例程】再稍加两处修改:
将【状态·过渡】成员方法
state_transition
从(【泛型·类型】+【泛型·类型·形参】)实现块impl<S1> Type1<S1>
搬移至(【泛型·类型】+【泛型·类型·实参】)实现块impl Type1<State1>
。于是,【状态·过渡】也就成为了每个【状态】的个性化行为了。将【状态·过渡】成员方法的【返回值·类型】从【泛型·类型·形参】替换为具体的【状态·类型】。
至此,一个完整的【例程】往这里看。
严格模式
在之前的例程中,【泛型·类型·参数】S1
能够接受任意【状态·类型】,而不管【泛型·类型】Type1<S1>
是否知道如何有效地处理它。这类完全开放式的程序设计并不满足日常生产的实际需求。通过给【泛型·类型·形参】S1
添加trait bound
限定条件,便可
禁止自定义【状态·类型】。比如,让编译器拒绝
Type1<State100>
,因为State100
并不是由“上游”程序代码预定义的【状态类型】,而是由“下游”开发者随意扩充的。上游代码不知道State100
的存在,和如何处理它。分组【状态·类型】。然后,给每一组【状态】定义(组)私有【成员方法】。
拒绝自定义【状态·类型】
就代码套路来讲,就三步:
给【状态·类型】实现某个自定义的
trait
。
后文称它为“【状态·类型】
trait
”。比如,trait State {}
。
密封该【状态·类型】trait
— 使其对外部程序可见·却·不可实现。
<往小处说>是【状态·类型】定义
module
之外的程序<往大处说>是【状态·类型】定义
crate
之外的程序总之,【外部程序】就是指“下游”代码
这里讲的【外部程序】:
具体作法就是:
于是,因为
supertrait
是私有的,所以subtrait
对外即便可见·也不可实现。
把【状态·类型】
trait
作为subtrait
。让其继承本地某个私有的
supertrait
。
给【泛型·类型】Type1<S1>
中的【泛型·类型·形参】S1
添加【状态·类型】trait
限定条件。比如,struct Type1<S1: State>
。
核心部分代码片段如下
-
/// “上游”代码定义【状态·类型】和它的`trait`
-
mod upstream {
-
/// 【状态·类型】
-
pub
struct State1 {
-
pub private_field1: String
// 【状态】独有【字段】
-
}
-
pub
struct State2 {
-
pub private_field2: String
// 【状态】独有【字段】
-
}
-
/// (私有的)密封`trait`
-
#[allow(private_in_public)]
-
trait Sealed {}
-
/// (公开的)【状态·类型】`trait`对外不可实现,
-
/// 因为它继承了(私有的)密封`trait`。
-
pub trait State: Sealed {}
-
/// 限定条件【状态·类型】
-
impl Sealed
for State1 {}
-
impl Sealed
for State2 {}
-
impl State
for State1 {}
-
impl State
for State2 {}
-
/// 借助`trait bound`,仅接收被`trait State`限定的
-
/// 内部定义的【状态·类型】。任意乱七八糟来源的【状态·类型】都
-
/// 会导致编译器报错。
-
pub
struct Type1<S1: State> {
-
pub state: S1,
-
pub com_field0: String
-
}
-
}
-
/// “下游”代码使用【状态·类型】,和扩充自定义【状态·类型】失败!
-
mod downstream {
-
use super::upstream::{State, State1, Type1};
-
struct State3;
-
// 因为`trait Sealed`对外不可见,所以`State3`不能被扩展为额外的【状态类型】
-
// impl State for State3 {}
-
pub fn main() {
-
let type1_state1: Type1<State1> = Type1 {
-
com_field0:
"对所有状态都看得到的,共用字段值".to_string(),
-
state: State1 {
-
private_field1:
"状态1私有字段值".to_string()
-
}
-
};
-
dbg!(&type1_state1.com_field0);
-
}
-
}
至此,一个完整的【例程】往这里看。
分组【状态·类型】灵活模式
在之前的例程中,【泛型·类型】Type1<S1>
的【成员方法】
要么,对所有【状态】都可见,
要么,仅对某个特定【状态】可见。
通过给【实现块】impl<S1> Type1<S1>
的【泛型·类型·形参】S1
添加【状态·类型】trait
限定条件,【成员方法】的可见范围就能够被限定于(同属一组的)某几个【状态】上。
就代码套路来讲,只需三步:
【准备】定义若干个
marker trait
分别代表不同的“分组”。比如,trait Group1 {}
。(见伪码#1
注释)【分组】给【状态·类型】实现不同的
marker trait
。(见伪码#2
注释)【就绪】定义包含了
marker trait
限定条件的【实现块】impl<S1: Group1> Type1<S1> {...}
。(见伪码#3
注释)
于是,在该【实现块】impl<S1: Group1> Type1<S1>
内定义的【成员方法】就仅只对组内若干个【状态】可见了。
-
/// 【状态·类型】
-
struct State1 {
-
private_field1: String
// 【状态】独有【字段】
-
}
-
struct State2 {
-
private_field2: String
// 【状态】独有【字段】
-
}
-
struct State3 {
-
private_field3: String
// 【状态】独有【字段】
-
}
-
// #1 定义·分组`marker trait`
-
trait Group1 {}
-
// #2 分组【状态·类型】
-
impl Group1
for State2 {}
-
impl Group1
for State3 {}
-
/// 【泛型·类型】+【泛型·类型·形参】
-
struct Type1<S1> {
// <- 被参数化的【状态·类型】既作为【泛型·类型·参数】,
-
state: S1,
// <- 也作为【状态·字段】的字段类型
-
com_field0: String
// 所有状态共有的【字段】
-
}
-
impl<S1> Type1<S1> {
// 所有状态共有的【成员方法】
-
fn com_function0(&self) -> String {
"全状态可调用".to_string()}
-
}
-
/// #3 【分组】内共有的【成员方法】
-
impl<S1> Type1<S1>
-
where S1: Group1 {
// Group1 状态共有的【成员方法】
-
fn group_function1(&self) -> String {
"Group1 状态可调用".to_string()}
-
}
-
/// 【状态】`State1`独有【成员方法】,对任何其它状态都不可见
-
impl Type1<State1> {
//
-
fn private_function1(&self) -> String {
"仅 State1 状态可调用".to_string()}
-
}
-
/// 【状态】`State2`独有【成员方法】,对任何其它状态都不可见
-
impl Type1<State2> {
-
fn private_function2(&self) -> String {
"仅 State2 状态可调用".to_string()}
-
}
-
// 状态1 实例
-
let type1_state1: Type1<State1> = Type1 {
-
com_field0:
"对所有状态都看得到的,共用字段值".to_string(),
-
state: State1 {private_field1:
"状态1私有字段值".to_string()}
-
};
-
dbg!(&type1_state1.com_field0);
-
dbg!(type1_state1.com_function0());
-
// 对非 Group1 的状态,没有 group_function1() 成员方法
-
// dbg!(type1_state1.group_function1());
-
// 状态2 实例
-
let type1_state2: Type1<State2> = Type1 {
-
com_field0:
"对所有状态都看得到的,共用字段值".to_string(),
-
state: State2 {private_field2:
"状态1私有字段值".to_string()}
-
};
-
dbg!(type1_state2.group_function1());
至此,一个完整的【例程】往这里看。
性能优化
零抽象成本的【状态】字段
此优化方法仅适用于unit type
【状态·字段】。一旦不需要依靠【状态】自身的存储力(即,S1
没有字段),那么【泛型·类型】Type1<S1>
中的【状态·字段】state
就蜕变成了【编译时】仅供rustc
理解源码的“分类标记flag
”,而不是【运行时】赋值/比较的状态变量。其作用与【解析几何】中的“辅助线”无异,帮助rustc
读懂我们的代码,然后即被抛弃掉。
在Rust
语法中,将结构体·字段定义为“标记flag
”,仅需将该字段·数据类型限定为std::marker::PhantomData<T>
即可。比如,
-
use ::std::marker::PhantomData;
-
struct Type1<S1> {
-
state: PhantomData<S1>,
-
com_field0: String
-
}
于是,在编译后,字段state
便会
从机器码内被删除
避免任何的【运行时】存储开销
而在编译过程中,rustc
会把它当作【单态化】新类型的“辅助线”。
-
use ::std::marker::PhantomData;
-
/// 【状态·类型】
-
struct State1;
// 无·独有·字段。仅做分类描述。编译后,其就没了。
-
struct State2;
-
/// 【泛型·类型】+【泛型·类型·形参】
-
struct Type1<S1> {
// <- 被参数化的【状态·类型】既作为【泛型·类型·参数】,
-
state: PhantomData<S1>,
// <- 也作为【状态·字段】的字段类型
-
com_field0: String
// 所有状态共有的【字段】
-
}
-
/// 所有状态共有的【成员方法】
-
impl<S1> Type1<S1> {
-
fn com_function0(&self) -> String {
"全状态可调用".to_string()}
-
}
-
/// 【状态】`State1`独有【成员方法】,对任何其它状态都不可见
-
impl Type1<State1> {
-
fn private_function1(&self) -> String {
"仅 State1 状态可调用".to_string()}
-
}
-
/// 【状态】`State2`独有【成员方法】,对任何其它状态都不可见
-
impl Type1<State2> {
-
fn private_function2(&self) -> String {
"仅 State2 状态可调用".to_string()}
-
}
-
// ---- 实例化【状态1】
-
let type1_state1: Type1<State1> = Type1 {
-
com_field0:
"对所有状态都看得到的,共用字段值".to_string(),
-
state: PhantomData
-
};
-
// 即便对 Type1<State2> 实例,此【成员方法】调用也是成立的。
-
dbg!(type1_state1.com_function0());
-
// 对 Type1<State2> 实例,此会报编译错误。
-
dbg!(type1_state1.private_function1());
至此,一个完整的【例程】往这里看。
按【引用】存储·状态共有【字段值】
此方案的优化点就是:避免在【状态·过渡】期间从【旧·状态】Type1<State1>
实例向【新·状态】Type1<State2>
实例move
大数据结构的(状态)共有【字段值】com_field0
,因为Type State pattern
【状态·过渡】就是“弃旧建新,继承数据”的过程。
优化思路就是:
首先,在【泛型·类型】
Type1<S1>
中,不直接保存【字段·所有权·值】本尊com_field0: String
,而仅缓存【所有权·值】的指针。然后,【字段·所有权·值】的指针
既可以是:【智能指针】
com_field0: Box<String>
也能够是:附有
lifetime
限定条件的普通引用com_field0: &'a String
按【智能指针】存储·状态共有【字段值】
优点:
实现简单 — 只需修改【字段·数据类型】与【构造函数】。借助于
Rust
【自动·解引用】语法糖,com_field0
字段的使用几乎不受影响。
缺点:
将字段值从【栈】上搬运到【堆】上,造成了一次【堆】分配的运行时开销
在【栈】上,【智能指针】多少还是要比【普通·引用】占用的内存空间大一些
因为比较简单,所以它没有单独的例程。但,在综合例程中,我以智能指针Arc<Mutex<T>>
来缓存多状态共用字段值。
按【普通·引用】保存·状态共有【字段值】
优点:
在【栈】上搞定一切的极致性能优化。但,请理性评估代码复杂度,客观掂量是否划得来。应用程序慢点又不是世界末日。Take easy!
缺陷:
花式操作【生命周期·限定条件】。考验咱们
rust
编程基本功的时刻到了。
这还真值得给一个完整的例程,如下:
-
use ::std::marker::PhantomData;
-
/// 【状态·类型】
-
struct State1;
// 无·独有·字段。仅做分类描述。编译后,其就没了。
-
struct State2;
-
/// 【泛型·类型】+【泛型·类型·形参】
-
struct Type1<
'a, S1> { // 多了一个【泛型·生命周期·参数】
-
state: PhantomData<S1>,
-
com_field0: &'a String
// 状态共有字段值是【引用】,而不是字符串自身
-
}
-
/// 所有【状态】共有的【成员方法】
-
impl<
'a, S1> Type1<'a, S1> {
-
fn com_function0(&self) -> String {
-
"全状态可调用".to_string()
-
}
-
}
-
/// 【状态】`State1`独有【成员方法】,对任何其它状态都不可见
-
impl<
'a> Type1<'a, State1> {
-
fn private_function1(&self) -> String {
-
"仅 State1 状态可调用".to_string()
-
}
-
}
-
/// 【状态】`State2`独有【成员方法】,对任何其它状态都不可见
-
impl<
'a> Type1<'a, State2> {
-
fn private_function2(&self) -> String {
-
"仅 State2 状态可调用".to_string()
-
}
-
}
-
/// 【状态】`State2`独有【状态·过渡】函数被以【类型转换】的方
-
/// 式实现。即,从【状态1】到【状态2】的类型转换。
-
impl<
'a> From<Type1<'a, State1>>
for Type1<
'a, State2> {
-
fn from(src: Type1<'a, State1>) -> Self {
-
Type1 {
-
state: PhantomData,
-
com_field0: src.com_field0
// 这里被搬运不是大字符串 String,
-
// 而仅只是一个普通的引用 &'a String
-
}
-
}
-
}
-
// ---- 实例化【状态1】
-
let com_field0 =
"对所有状态都看得到的,共用字段值".to_string();
-
let type1_state1: Type1<
'_, State1> = Type1 {
-
com_field0: &com_field0,
-
state: PhantomData
-
};
-
dbg!(type1_state1.com_field0);
-
dbg!(type1_state1.com_function0());
-
dbg!(type1_state1.private_function1());
-
// ---- 从【状态1】过渡至【状态2】
-
let type1_state2: Type1<'_, State2> = type1_state1.into();
-
dbg!(type1_state2.com_field0);
-
dbg!(type1_state2.com_function0());
-
dbg!(type1_state2.private_function2());
至此,一个完整的【例程】往这里看。
RAII
即是Type States
在Rust
中,RAII
就是【类型·状态·设计模式】只有两个状态(living / dead
或open / closed
)时的特例。据此,一旦【实例】进入后一个状态(dead / closed
),那么属于前一个状态(living / open
)的成员方法与关联函数就都不可见与不可调用了 — 这也是Rust
承诺的安全特性之一。比如,从被关闭的数据库连接实例上“点”execute_sql(str)
成员方法,不用等运行时异常报bug
,编译器就会第一时间向你报怨“错了呀!”。
此外,若【实例】具有多个living
状态和一个dead
状态,这就是普通的【类型·状态·设计模式】。
综合例程
通过给“无人机·飞行控制”建立【程序模型】,集中展现【类型·状态】设计模式的完整编码套路。在此例程中,被涉及到的技术知识点包括:
零抽象成本·状态字段(见
Flying<S: Motionless>.destination_state
)按【智能指针】存储的多个状态共有字段值(见
Drone<S: State>.coordinate
)密封【状态类型】禁止下游代码额外扩展(见
seal_by_trait!()
宏)【状态类型】分组(见
group_by_trait!()
宏)【状态组】独有成员方法(见
Drone<S: Midair>::take_picture(&self)
和Drone<Flying<S: Motionless>>::inner_fly(mut self, state, step)
)【状态】独有成员方法(见
Drone<Idle>::take_off(self)
)【状态】独有数据字段(见
Flying<S: Motionless>.origin
)编译时“多态”的【状态过渡】(见
Drone<Flying<Idle>>::fly(mut self, step)
和Drone<Flying<Hovering>>::fly(mut self, step)
)intra-doc link
文档注释指令
【无人机】总共在三个状态之间切换:
待命
Idle
—— 无人机·在地面上飞行
Flying
—— 无人机·空中飞行悬浮
Hovering
—— 无人机·静止于空中
上述三个状态又分成了两组:
“静止”组
Motionless
,包括Idle
和Hovering
Flying
的紧下一个状态必须是Motionless
的。
“空中”组Midair
,包括Flying
和Hovering
空中的无人机有一个额外的功能就是“拍照”。
【无人机】三个状态各自还有独特的行为:
Idle
有take_off()
起飞·行为,从而将Idle
状态过渡为Flying
Hovering
有两个行为,从而将
Hovering
状态过渡为Flying
move_to()
前往land()
着落
Flying
有fly()
飞行·行为。该行为
用跨线程【迭代器】模拟【无人机】(缓慢)飞行过程。
若紧前状态是
Idle
,那么紧后状态一定是Hovering
。即,Idle -> Flying -> Hovering
若紧前状态是
Hovering
,那么状态过渡目标既有可能是Idle
,还可能还是Hovering
。这取决于之前Hovering
是如何过渡到Flying
的。既是异步的:
还是多态的:
完整的【例程】往这里看。
结束语
这篇文章是我2022
年的收官之作,希望能够给大家带来更多启发与帮助。大家来点赞,留言呀!
转载:https://blog.csdn.net/u012067469/article/details/128463073