飞道的博客

[Rust笔记] 对照 OOP 浅谈【类型状态】设计模式

397人阅读  评论(0)

对照OOP浅谈【类型状态】设计模式

类型状态·设计模式Type State Pattern也被称作“泛型·即是·类的类型(约束)Generic as Type Class (Constraint)”。它是基于Rust独有语言特性

  • 单态化monomorphization

  • move赋值语义

的新颖设计模式。其中,【move赋值语义】为Rust所独有的原因是

  • 一方面,GC类计算机语言已经将内存托管给vm,所以他们就没再搞出这类复杂的内存管理概念,和增加开发者的心智负担。

  • 另一方面,Cppmove赋值语义仅只是对历史包袱的【妥协方案】。这体现在

    所以,和Rust相比,Cppmove赋值语义至多就是一个“弟弟”。其功能相当于Rust标准库提供的std::mem::take(&T) -> T内存操作 — 使用【类型·默认值】置换出【引用】内存位置上的值;同时,保留·原变量·的【所有权】不被消耗掉和可以被接着使用。

    • Cppmove语义是: 用空指针nullptr换走原变量的值;但,原变量依旧可访问。这哪里是move,分明是swap呀!

    • Rustmove语义是:拿走原变量的值;同时,作废原变量。这个操作也被称为“消耗consuming”。

  • 此外,move也不是Cpp变量赋值的默认语义。相反 ,开发者得显示地编码std::move(ptr)函数调用和将lvalue转换为rvalue

    Cppstd::move(ptr)函数调用是【零·运行时·成本】的。在编译之后,编译器会将其从机器码内扣掉。其“辅助线”般的功能有些类似于Rust中的std::marker::PhantomData<T>

名词解释

为了避免后续文章内容过于啰嗦,首先定义五个词条,包括:

  • 泛型类型

  • 泛型类型参数

  • 泛型类型

  • 泛型类型

  • 泛型类型参数限定条件

一图抵千词,大家来看下图吧:

基本概念

【类型状态·模式】的初衷是:将【运行时】对象状态的(动态)信息·编码入·【编译时】对象类型的(静态)定义里。进而,借助现成且完备的Rust【类型系统】,在【编译】过程中,确保:

  1. 处于不同状态的(泛型类型)实例·拥有不一样的(【成员方法】+【关联函数】+【字段】)集合。

  2. (泛型类型)实例·仅能在毗邻的状态之间进行“状态·过渡”,而不能“跳变”。

  3. 排查出·状态的成员方法调用。比如,A状态的实例调用了仅在B状态才有效的成员方法。 而不是,让这类错误潜伏着和等【测试覆盖】或抛出【运行时·异常】。

以【订单系统】为例,【编译器】就能筛查出代码里

  • 对【无效订单】实例的【发货】成员方法调用

  • 对【出库订单】实例的【完成】成员方法调用 — 还未经历【发货】与【收款】两个状态

相对于传统的OOP程序,Rust【类型状态】设计模式将【对象·状态】的【运行时】检查前置于【编译环节】。进而带来的好处包括但不限于:

  • 将【运行时】程序崩溃“无害化”为【编译时】错误。

    • 就开发者而言,这意味着更短的【思考+试错】反馈回路。

    • 就应用程序而言,这意味着更高的性能,更健壮的可靠性,和更重的应用程序大小 — 【单态化】的本质就是以空间换时间

  • 允许IDE提供更有价值的代码提示。即,仅智能地列出对当前状态实例有效的【成员方法】,而不是罗列全部成员方法。比如,当开发者“点”一个【无效订单】实例时,IDE就不应该提示出【发货】成员方法。这才是对开发者最实在的帮助。

代码套路

从操作细节来说,为了采用【类型状态·设计模式】,我们需要:

  1. 将每个【状态】分别映射为独立的【结构体】(比如,struct State1)。和在结构体内,定义【状态】独有的:字段。(见伪码#1注释)

    1. 而不是,使用一个【枚举类】enum State {...}笼统地描述所有【状态】

    2. 后文称这类【结构体】为【状态·类型】。

  2. 以【泛型·类型】+【泛型·类型·参】的结构体定义(比如,struct Type1<S1>),抽象所有【状态】共有的:字段。(见伪码#2注释)

  3. 以【泛型·类型】+【泛型·类型·参】的实现块(比如,impl<S1> Type1<S1>),抽象所有【状态】共有的:成员方法,关联函数,关联常量,和关联类型。(见伪码#3注释)

  4. 以【泛型·类型】+【泛型·类型·参】的实现块(比如,impl Type1<State1>),定制每个【状态】独有的:成员方法,关联函数,关联常量,和关联类型。(见伪码#4注释)


   
  1. /// #1 【状态·类型】
  2. struct State1 {
  3. private_field1: String // 定义【状态】独有【字段】
  4. }
  5. struct State2 {
  6. private_field2: String // 定义【状态】独有【字段】
  7. }
  8. /// #2 【泛型·类型】+【泛型·类型·形参】
  9. struct Type1<S1> { // <- 被参数化的【状态·类型】既作为【泛型·类型·参数】,
  10. state: S1, // <- 也作为【状态·字段】的字段类型
  11. com_field0: String // 抽象全部【状态】共有【字段】
  12. }
  13. /// #3 【泛型·类型】+【泛型·类型·形参】
  14. impl<S1> Type1<S1> { // 抽象全部【状态】共有的【成员方法】
  15. fn com_function0(&self) -> String {
  16. "全状态可调用".to_string()
  17. }
  18. }
  19. /// #4 【泛型·类型】+【泛型·类型·实参】
  20. impl Type1<State1> { // 定制【状态】`State1`独有【成员方法】
  21. fn private_function1(&self) -> String {
  22. "仅 State1 状态可调用".to_string()
  23. }
  24. }
  25. impl Type1<State2> { // 定制【状态】`State2`独有【成员方法】
  26. fn private_function2(&self) -> String {
  27. "仅 State2 状态可调用".to_string()
  28. }
  29. }

承上段代码,在【泛型·类型】struct Type1<S1>中,被参数化的【状态·类型】S1既作为【泛型·类型·参数】也作为【状态·字段】state的字段类型(这是由Generic Struct定义要求的 — 在结构体定义中,被声明的泛型参数必须被使用)。


   
  1. // 继续前面的代码
  2. let type1_state1 = Type1 {
  3. com_field0: "对所有状态都看得到的,共用字段值".to_string(),
  4. // 锚定 type1_state1 实例处于 State1 状态
  5. state: State1 {
  6. private_field1: "状态1的私有字段值。对其它任何状态都不可见".to_string()
  7. }
  8. };
  9. // 即便对 Type1<State2> 实例,此【成员方法】调用也是成立的。
  10. dbg!(type1_state1.com_function0());
  11. // 对 Type1<State2> 实例,此会报编译错误。
  12. dbg!(type1_state1.private_function1());
  13. // 对【状态】独有【字段】的取值语句则有些“啰嗦”了。
  14. dbg!(&type1_state1.state.private_field1[..]);

承上段代码,除了【状态】State1的独有【字段】private_field1需要隔着一层【状态·字段】state取值(如,type1_state1.state.private_field1),所有其它的【项】都能从type1_state1实例上直接“点出来”(如,type1_state1.private_function1())。

虽然【状态】独有【字段】的取值语句有些冗长,但语法是“死”的,可人是“活”的呀!再额外封装一个【状态】独有getter【成员方法】即可简化字段取值操作。


   
  1. // 继续前面的代码
  2. impl Type1<State1> {
  3. // 既可缩短【状态】独有字段的取值路径,
  4. // 也可抹掉 <Type1>.state 与 <State1>.private_field1 字段的`pub`可见性修饰符。
  5. fn private_field1(&self) -> &str {
  6. &self.state.private_field1[..]
  7. }
  8. }
  9. // 取值语句是不是精简多了?此外,该成员方法对 Type1<State2> 类型实例不可见。
  10. dbg!(type1_state1.private_field1());

至此,一个完整的【例程】往这里看。

代码结构·示意图

文档注释小技巧

将描述【状态】含义的doc comments放在(【泛型·类型】+【泛型·类型·参】)实现块impl Type1<State1>的上端,而不是在【状态·类型】结构体定义struct State1之上。那么, rustdoc便会把全部【状态】的doc comments文案都收拢于一个html文档页内(即,【泛型·类型】struct Type1<S1>文档页内里),而不是分散于多个html页。这给“下游”开发者提供了更友好的文档阅读连贯性。


   
  1. /// 将描述【状态】的【文档注释】加在这里,
  2. impl Type1<State1> {...}
  3. /// 而不是加在这里。借助`intra-doc link`注释指令:[`Type1<State1>`](struct@crate::Type1#impl-Type1<State1>)
  4. /// 你可以直接链接到上面`impl Type1<State1> {...}`的位置。
  5. 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程序的反面例子。


   
  1. /// 警告:不推荐这么写。这仅只是一个反面例子。
  2. ///
  3. /// 假设一共有三个状态
  4. enum State {
  5. State1,
  6. State2,
  7. StateN
  8. }
  9. struct Type1 {
  10. // 【枚举类·字段类型】笼统地概括了所有可能的【状态】
  11. // 或者讲,所有的【状态】都是同一个类型。
  12. state: State
  13. }
  14. impl Type1 {
  15. // 根据设计,该成员方法仅只对`State1`状态的实例有效。
  16. // 没有了【类型·状态】设计模式的赋能,`operate1()`成员方法便保证不了“调用安全”。
  17. fn operate1(&mut self) {
  18. // 运行时【防御性·判断】造成了
  19. match &self.state { // (1)重复的代码
  20. State::State1 => {
  21. // (2)更深的缩进
  22. },
  23. _ => { // (3)潜在的崩溃点
  24. panic!( "我不能工作于 State2 与 State3 状态");
  25. // 若能在 State2 与 State3 状态就点不到`operate1()`成员方法,
  26. // 那就完美了。
  27. }
  28. };
  29. }
  30. }

type state模式的程序相比,此处的OOP代码一下子就少了fluent的感觉了。试想每个成员方法都如operate1()这般臃肿,那将是多么令人烦躁的困境。

OOP泛型

Rust相比 ,cpp/java【泛型·类型】的“形状”(即,成员方法+字段·的集合)永远是相同的,无论【泛型·类型·参】被实际代入什么【具体类型】。这是因为

  • Rust — 在【编译】语法分析阶段,借助于AST,安全地生成新类型定义(单态化)。这不仅仅是代换入【泛型·类型·实参】这么初级。相反,每对(【泛型·类型】+【泛型·类型·实参】)组合都是拥有新成员方法(和关联函数)的新类型。

  • Cpp — 在【编译】词法分析阶段,以“字符串替换”方式,将模板内的“占位符”安全地·调换为·具体“类型名”。这既没有对旧类型“形状”的裁剪,也没有对新类型的定义。

  • Java — 在【运行时】,将·具体值·代入Class Object的泛型参数。无从谈起,新建或改变【类型定义】,因为Java又不是动态语言。

所以,在这里再次重申我的观点:请不要吐槽rustc编译时间长了。你看它在【编译】期间明显地完成了更多得多的工作。这怎么可能有闪电般的编译速度呢?如果你的项目与团队对程序的编译延时有着非常苛刻的要求,没准你们需要“换一个脚本语言技术栈”。

OOP状态字段

在仅OOP的结构体定义中,【状态·字段】被设计为一个【枚举类】enum State {State1, State2, StateN}和以一个类型笼统地描述所有【状态】,所以

  1. 不再需要【泛型·类型·参数】S1了。上例中的Type1结构体也不是【泛型·类型】,而是普通结构体struct Type1了。

    
         
    1. /// 【枚举类】笼统地概括了所有可能的【状态】
    2. /// 或者讲,所有的【状态】都是同一个类型。
    3. enum State {
    4. State1,
    5. State2,
    6. StateN
    7. }
    8. /// 不再是【泛型类型】了
    9. struct Type1 {
    10. com_field0: String,
    11. state: State // 状态字段
    12. }
    13. impl Type1 {
    14. fn operate1(&mut self) {
    15. // 1. 防御性·判断
    16. // 2. 真正的业务逻辑代码
    17. }
    18. }
  2. rustc不会凭借【单态化】与【泛型·类型·实参】生成新类型了。

  3. 不再保证Method调用安全,因为每个状态的结构体实例都能“点”出全部的【成员方法】,而不论被“点”出的成员方法与当前状态是否匹配。

  4. 仅仅修改【状态·字段】的值,即可实现【状态·过渡】。而Rust类型状态设计模式却要求【状态·过渡】是“建新,弃旧;move数据”的过程(详细见下文)。

  5. 状态字段也不再是零抽象成本了

总之,Rust类型状态设计模式与OOP仅有一分相似却带九分不同:OOP是·运行时·多态,而Type State pattern是·编译时·多态。

状态·过渡

迥异于OOP程序直接修改【状态·字段】的值self.state = State::State2;Rust【类型·状态】设计模式则要求:

  1. 构造【新】状态的【新】实例(见伪码#1注释)

  2. 消费掉【旧】状态的【旧】实例(见伪码#2注释)。进而,将旧状态的字段值com_field0按值传递给新状态实例。

其背后的逻辑是:

  1. Type1<State1>Type1<State2>是两个不同的类型。

  2. State1State2的状态过渡就是从Type1<State1>Type1<State2>类型转换 — 更Rustacean的表述就是impl From<Type1<State1>> for Type1<State2> {...}

  3. OOP才用一个枚举类enum State“笼统地”概括所有【状态】。然后,修改Type1.state状态字段值·实现·状态过渡。


   
  1. // 状态共有·实现块
  2. impl<S1> Type1<S1> {
  3. // 【状态·过渡】泛型函数 — 对所有状态都可见
  4. // #2 消耗型成员方法 - 消费掉【旧】状态的【旧】实例
  5. fn state_transition<NextState>(self, new_state: NextState) -> Type1<NextState> {
  6. // #1 构造【新】状态的【新】实例
  7. Type1 {
  8. state: new_state, // 代入下一个状态的【泛型·类型·实参】
  9. com_field0: self.com_field0 // 同时“移入”状态共有的【字段值】。
  10. // 注意:这里不能使用`..self`解构语法是因为`Type1<State1>`
  11. // 与`Type1<State1>`的类型不同
  12. }
  13. // self.state = new_state; 会导致编译失败,因为类型不匹配。
  14. }
  15. }

至此,一个完整的【例程】往这里看。

在文章开篇就强调过:“【类型·状态】设计模式能够在【编译时】就筛查出无关【状态】之间的错误跳变”。为了具备这个能力,仅需对上面【例程】再稍加两处修改:

  1. 将【状态·过渡】成员方法state_transition从(【泛型·类型】+【泛型·类型·参】)实现块impl<S1> Type1<S1>搬移至(【泛型·类型】+【泛型·类型·参】)实现块impl Type1<State1>。于是,【状态·过渡】也就成为了每个【状态】的个性化行为了。

  2. 将【状态·过渡】成员方法的【返回值·类型】从【泛型·类型·形参】替换为具体的【状态·类型】。

至此,一个完整的【例程】往这里看。

严格模式

在之前的例程中,【泛型·类型·参数】S1能够接受任意【状态·类型】,而不管【泛型·类型】Type1<S1>是否知道如何有效地处理它。这类完全开放式的程序设计并不满足日常生产的实际需求。通过给【泛型·类型·形参】S1添加trait bound限定条件,便可

  • 禁止自定义【状态·类型】。比如,让编译器拒绝Type1<State100>,因为State100并不是由“上游”程序代码预定义的【状态类型】,而是由“下游”开发者随意扩充的。上游代码不知道State100的存在,和如何处理它。

  • 分组【状态·类型】。然后,给每一组【状态】定义(组)私有【成员方法】。

拒绝自定义【状态·类型】

就代码套路来讲,就三步:

  1. 给【状态·类型】实现某个自定义的trait

  • 后文称它为“【状态·类型】trait”。比如,trait State {}

密封该【状态·类型】trait — 使其对外部程序可见·却·不可实现

  • <往处说>是【状态·类型】定义module之外的程序

  • <往处说>是【状态·类型】定义crate之外的程序

  • 总之,【外部程序】就是指“下游”代码

  • 这里讲的【外部程序】:

  • 具体作法就是:

  • 于是,因为supertrait私有的,所以subtrait对外即便可见·也不可实现

  1. 把【状态·类型】trait作为subtrait

  2. 让其继承本地某个私有的supertrait

给【泛型·类型】Type1<S1>中的【泛型·类型·形参】S1添加【状态·类型】trait限定条件。比如,struct Type1<S1: State>

核心部分代码片段如下


   
  1. /// “上游”代码定义【状态·类型】和它的`trait`
  2. mod upstream {
  3. /// 【状态·类型】
  4. pub struct State1 {
  5. pub private_field1: String // 【状态】独有【字段】
  6. }
  7. pub struct State2 {
  8. pub private_field2: String // 【状态】独有【字段】
  9. }
  10. /// (私有的)密封`trait`
  11. #[allow(private_in_public)]
  12. trait Sealed {}
  13. /// (公开的)【状态·类型】`trait`对外不可实现,
  14. /// 因为它继承了(私有的)密封`trait`。
  15. pub trait State: Sealed {}
  16. /// 限定条件【状态·类型】
  17. impl Sealed for State1 {}
  18. impl Sealed for State2 {}
  19. impl State for State1 {}
  20. impl State for State2 {}
  21. /// 借助`trait bound`,仅接收被`trait State`限定的
  22. /// 内部定义的【状态·类型】。任意乱七八糟来源的【状态·类型】都
  23. /// 会导致编译器报错。
  24. pub struct Type1<S1: State> {
  25. pub state: S1,
  26. pub com_field0: String
  27. }
  28. }
  29. /// “下游”代码使用【状态·类型】,和扩充自定义【状态·类型】失败!
  30. mod downstream {
  31. use super::upstream::{State, State1, Type1};
  32. struct State3;
  33. // 因为`trait Sealed`对外不可见,所以`State3`不能被扩展为额外的【状态类型】
  34. // impl State for State3 {}
  35. pub fn main() {
  36. let type1_state1: Type1<State1> = Type1 {
  37. com_field0: "对所有状态都看得到的,共用字段值".to_string(),
  38. state: State1 {
  39. private_field1: "状态1私有字段值".to_string()
  40. }
  41. };
  42. dbg!(&type1_state1.com_field0);
  43. }
  44. }

至此,一个完整的【例程】往这里看。

分组【状态·类型】灵活模式

在之前的例程中,【泛型·类型】Type1<S1>的【成员方法】

  • 要么,对所有【状态】都可见,

  • 要么,仅对某个特定【状态】可见。

通过给【实现块】impl<S1> Type1<S1>的【泛型·类型·形参】S1添加【状态·类型】trait限定条件,【成员方法】的可见范围就能够被限定于(同属一组的)某几个【状态】上。

就代码套路来讲,只需三步:

  1. 【准备】定义若干个marker trait分别代表不同的“分组”。比如,trait Group1 {}。(见伪码#1注释)

  2. 【分组】给【状态·类型】实现不同的marker trait。(见伪码#2注释)

  3. 【就绪】定义包含了marker trait限定条件的【实现块】impl<S1: Group1> Type1<S1> {...}。(见伪码#3注释)

于是,在该【实现块】impl<S1: Group1> Type1<S1>内定义的【成员方法】就仅只对组内若干个【状态】可见了。


   
  1. /// 【状态·类型】
  2. struct State1 {
  3. private_field1: String // 【状态】独有【字段】
  4. }
  5. struct State2 {
  6. private_field2: String // 【状态】独有【字段】
  7. }
  8. struct State3 {
  9. private_field3: String // 【状态】独有【字段】
  10. }
  11. // #1 定义·分组`marker trait`
  12. trait Group1 {}
  13. // #2 分组【状态·类型】
  14. impl Group1 for State2 {}
  15. impl Group1 for State3 {}
  16. /// 【泛型·类型】+【泛型·类型·形参】
  17. struct Type1<S1> { // <- 被参数化的【状态·类型】既作为【泛型·类型·参数】,
  18. state: S1, // <- 也作为【状态·字段】的字段类型
  19. com_field0: String // 所有状态共有的【字段】
  20. }
  21. impl<S1> Type1<S1> { // 所有状态共有的【成员方法】
  22. fn com_function0(&self) -> String { "全状态可调用".to_string()}
  23. }
  24. /// #3 【分组】内共有的【成员方法】
  25. impl<S1> Type1<S1>
  26. where S1: Group1 { // Group1 状态共有的【成员方法】
  27. fn group_function1(&self) -> String { "Group1 状态可调用".to_string()}
  28. }
  29. /// 【状态】`State1`独有【成员方法】,对任何其它状态都不可见
  30. impl Type1<State1> { //
  31. fn private_function1(&self) -> String { "仅 State1 状态可调用".to_string()}
  32. }
  33. /// 【状态】`State2`独有【成员方法】,对任何其它状态都不可见
  34. impl Type1<State2> {
  35. fn private_function2(&self) -> String { "仅 State2 状态可调用".to_string()}
  36. }
  37. // 状态1 实例
  38. let type1_state1: Type1<State1> = Type1 {
  39. com_field0: "对所有状态都看得到的,共用字段值".to_string(),
  40. state: State1 {private_field1: "状态1私有字段值".to_string()}
  41. };
  42. dbg!(&type1_state1.com_field0);
  43. dbg!(type1_state1.com_function0());
  44. // 对非 Group1 的状态,没有 group_function1() 成员方法
  45. // dbg!(type1_state1.group_function1());
  46. // 状态2 实例
  47. let type1_state2: Type1<State2> = Type1 {
  48. com_field0: "对所有状态都看得到的,共用字段值".to_string(),
  49. state: State2 {private_field2: "状态1私有字段值".to_string()}
  50. };
  51. dbg!(type1_state2.group_function1());

至此,一个完整的【例程】往这里看。

性能优化

零抽象成本的【状态】字段

此优化方法仅适用于unit type【状态·字段】。一旦不需要依靠【状态】自身的存储力(即,S1没有字段),那么【泛型·类型】Type1<S1>中的【状态·字段】state就蜕变成了【编译时】仅供rustc理解源码的“分类标记flag”,而不是【运行时】赋值/比较的状态变量。其作用与【解析几何】中的“辅助线”无异,帮助rustc读懂我们的代码,然后即被抛弃掉。

Rust语法中,将结构体·字段定义为“标记flag”,仅需将该字段·数据类型限定为std::marker::PhantomData<T>即可。比如,


   
  1. use ::std::marker::PhantomData;
  2. struct Type1<S1> {
  3. state: PhantomData<S1>,
  4. com_field0: String
  5. }

于是,在编译后,字段state便会

  • 从机器码内被删除

  • 避免任何的【运行时】存储开销

而在编译过程中,rustc会把它当作【单态化】新类型的“辅助线”。


   
  1. use ::std::marker::PhantomData;
  2. /// 【状态·类型】
  3. struct State1; // 无·独有·字段。仅做分类描述。编译后,其就没了。
  4. struct State2;
  5. /// 【泛型·类型】+【泛型·类型·形参】
  6. struct Type1<S1> { // <- 被参数化的【状态·类型】既作为【泛型·类型·参数】,
  7. state: PhantomData<S1>, // <- 也作为【状态·字段】的字段类型
  8. com_field0: String // 所有状态共有的【字段】
  9. }
  10. /// 所有状态共有的【成员方法】
  11. impl<S1> Type1<S1> {
  12. fn com_function0(&self) -> String { "全状态可调用".to_string()}
  13. }
  14. /// 【状态】`State1`独有【成员方法】,对任何其它状态都不可见
  15. impl Type1<State1> {
  16. fn private_function1(&self) -> String { "仅 State1 状态可调用".to_string()}
  17. }
  18. /// 【状态】`State2`独有【成员方法】,对任何其它状态都不可见
  19. impl Type1<State2> {
  20. fn private_function2(&self) -> String { "仅 State2 状态可调用".to_string()}
  21. }
  22. // ---- 实例化【状态1】
  23. let type1_state1: Type1<State1> = Type1 {
  24. com_field0: "对所有状态都看得到的,共用字段值".to_string(),
  25. state: PhantomData
  26. };
  27. // 即便对 Type1<State2> 实例,此【成员方法】调用也是成立的。
  28. dbg!(type1_state1.com_function0());
  29. // 对 Type1<State2> 实例,此会报编译错误。
  30. 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编程基本功的时刻到了。

这还真值得给一个完整的例程,如下:


   
  1. use ::std::marker::PhantomData;
  2. /// 【状态·类型】
  3. struct State1; // 无·独有·字段。仅做分类描述。编译后,其就没了。
  4. struct State2;
  5. /// 【泛型·类型】+【泛型·类型·形参】
  6. struct Type1< 'a, S1> { // 多了一个【泛型·生命周期·参数】
  7. state: PhantomData<S1>,
  8. com_field0: &'a String // 状态共有字段值是【引用】,而不是字符串自身
  9. }
  10. /// 所有【状态】共有的【成员方法】
  11. impl< 'a, S1> Type1<'a, S1> {
  12. fn com_function0(&self) -> String {
  13. "全状态可调用".to_string()
  14. }
  15. }
  16. /// 【状态】`State1`独有【成员方法】,对任何其它状态都不可见
  17. impl< 'a> Type1<'a, State1> {
  18. fn private_function1(&self) -> String {
  19. "仅 State1 状态可调用".to_string()
  20. }
  21. }
  22. /// 【状态】`State2`独有【成员方法】,对任何其它状态都不可见
  23. impl< 'a> Type1<'a, State2> {
  24. fn private_function2(&self) -> String {
  25. "仅 State2 状态可调用".to_string()
  26. }
  27. }
  28. /// 【状态】`State2`独有【状态·过渡】函数被以【类型转换】的方
  29. /// 式实现。即,从【状态1】到【状态2】的类型转换。
  30. impl< 'a> From<Type1<'a, State1>> for Type1< 'a, State2> {
  31. fn from(src: Type1<'a, State1>) -> Self {
  32. Type1 {
  33. state: PhantomData,
  34. com_field0: src.com_field0 // 这里被搬运不是大字符串 String,
  35. // 而仅只是一个普通的引用 &'a String
  36. }
  37. }
  38. }
  39. // ---- 实例化【状态1】
  40. let com_field0 = "对所有状态都看得到的,共用字段值".to_string();
  41. let type1_state1: Type1< '_, State1> = Type1 {
  42. com_field0: &com_field0,
  43. state: PhantomData
  44. };
  45. dbg!(type1_state1.com_field0);
  46. dbg!(type1_state1.com_function0());
  47. dbg!(type1_state1.private_function1());
  48. // ---- 从【状态1】过渡至【状态2】
  49. let type1_state2: Type1<'_, State2> = type1_state1.into();
  50. dbg!(type1_state2.com_field0);
  51. dbg!(type1_state2.com_function0());
  52. dbg!(type1_state2.private_function2());

至此,一个完整的【例程】往这里看。

RAII即是Type States

Rust中,RAII就是【类型·状态·设计模式】只有两个状态(living / deadopen / closed)时的特例。据此,一旦【实例】进入后一个状态(dead / closed),那么属于前一个状态(living / open)的成员方法与关联函数就都不可见与不可调用了 — 这也是Rust承诺的安全特性之一。比如,从被关闭的数据库连接实例上“点”execute_sql(str)成员方法,不用等运行时异常报bug,编译器就会第一时间向你报怨“错了呀!”。

此外,若【实例】具有多个living状态和一个dead状态,这就是普通的【类型·状态·设计模式】。

综合例程

通过给“无人机·飞行控制”建立【程序模型】,集中展现【类型·状态】设计模式的完整编码套路。在此例程中,被涉及到的技术知识点包括:

  1. 零抽象成本·状态字段(见Flying<S: Motionless>.destination_state

  2. 按【智能指针】存储的多个状态共有字段值(见Drone<S: State>.coordinate

  3. 密封【状态类型】禁止下游代码额外扩展(见seal_by_trait!()宏)

  4. 【状态类型】分组(见group_by_trait!()宏)

  5. 【状态组】独有成员方法(见Drone<S: Midair>::take_picture(&self)Drone<Flying<S: Motionless>>::inner_fly(mut self, state, step)

  6. 【状态】独有成员方法(见Drone<Idle>::take_off(self)

  7. 【状态】独有数据字段(见Flying<S: Motionless>.origin

  8. 编译时“多态”的【状态过渡】(见Drone<Flying<Idle>>::fly(mut self, step)Drone<Flying<Hovering>>::fly(mut self, step)

  9. intra-doc link文档注释指令

【无人机】总共在三个状态之间切换:

  1. 待命Idle —— 无人机·在地面上

  2. 飞行Flying —— 无人机·空中飞行

  3. 悬浮Hovering —— 无人机·静止于空中

上述三个状态又分成了两组:

  1. “静止”组Motionless,包括IdleHovering

  • Flying的紧下一个状态必须是Motionless的。

“空中”组Midair,包括FlyingHovering

  • 空中的无人机有一个额外的功能就是“拍照”。

【无人机】三个状态各自还有独特的行为:

  1. Idletake_off()起飞·行为,从而将Idle状态过渡为Flying

  2. Hovering

    两个行为,从而将Hovering状态过渡为Flying

    1. move_to()前往

    2. land()着落

  3. Flyingfly()飞行·行为。该行为

  • 用跨线程【迭代器】模拟【无人机】(缓慢)飞行过程。

  1. 若紧状态是Idle,那么紧状态一定是Hovering。即,Idle -> Flying -> Hovering

  2. 若紧前状态是Hovering,那么状态过渡目标既有可能是Idle,还可能还是Hovering。这取决于之前Hovering是如何过渡到Flying的。

  3. 既是异步的:

  4. 还是多态的:

完整的【例程】往这里看。

结束语

这篇文章是我2022年的收官之作,希望能够给大家带来更多启发与帮助。大家来点赞,留言呀!


转载:https://blog.csdn.net/u012067469/article/details/128463073
查看评论
* 以上用户言论只代表其个人观点,不代表本网站的观点或立场