24 个版本 (6 个重大更新)
0.9.0-rc2 | 2021 年 9 月 2 日 |
---|---|
0.8.0 | 2021 年 7 月 16 日 |
0.1.3-alpha | 2020 年 11 月 27 日 |
951 在 Rust 模式 中
每月 37 次下载
在 3 个包(2 个直接)中使用
300KB
182 行
#[typestate]
此库为开发人员提供宏来设计类型状态对象。
[dependencies]
typestate = "0.9.0-rc2"
编译器支持:需要 rustc 1.42+
简介
你在 Java 中对 IllegalStateException
感到沮丧吗?
类型状态允许你为对象定义 安全 的使用协议。编译器将帮助你在旅途中,并禁止给定状态上的错误。你将无法再尝试从关闭的流中读取。
#[typestate]
基于 state_machine_future
包的思想。
Rust 中的类型状态
类型状态并不是 Rust 的新概念。关于这个主题有多个博客文章[1, 2, 3],以及《嵌入式 Rust 书》中的章节。
简而言之,我们可以手动编写类型状态,添加一些泛型,将其声明为 "状态",然后我们可以继续使用我们新的状态机。
然而,这种方法是 容易出错 和 冗长 的(特别是对于更大的自动机)。它还提供了 没有 关于自动机的保证,除非当然,你之前设计和测试了设计。
作为程序员,我们想要自动化这项繁琐的工作,为此,我们使用 Rust 的强大过程宏!
基本指南
假设我们被要求构建交通灯的固件,我们可以将其打开和关闭,并在绿色、黄色和红色之间循环。
我们首先使用 #[typestate]
宏声明一个模块。
#[typestate]
mod traffic_light {}
当然,这不会做任何事情,事实上它会导致一个错误,提示我们没有声明一个自动机。
因此,我们的下一个任务是完成这个任务。在traffic_light
模块内部,我们声明一个结构体,并使用#[automaton]
进行标注。
#[automaton]
pub struct TrafficLight;
我们的下一步是声明状态。我们声明了三个空的结构体,并使用"[state]
进行标注。
#[state] pub struct Green;
#[state] pub struct Yellow;
#[state] pub struct Red;
到目前为止,一切顺利,然而应该会出现一些错误,关于缺少初始和最终状态。
要声明初始和最终状态,我们需要将它们视为可以通过转换来描述的。每当创建一个对象时,创建它的方法将对象置于初始状态。同样,每当一个方法消耗一个对象并且不返回它(或它的类似版本),它就会使对象达到最终状态。
有了这个想法,我们可以制定以下规则
- 不接收有效状态(即
self
)并返回有效状态的函数,描述了一个初始状态。 - 接收有效状态(即
self
)但不返回有效状态的函数,描述了一个最终状态。
因此,我们编写以下函数签名
fn turn_on() -> Red;
fn turn_off(self);
然而,这些是自由函数,意味着self
与任何东西无关。为了将它们附加到状态,我们用名为它们应该附加到的状态的特质
将它们包装起来。因此,我们之前的示例变为
trait Red {
fn turn_on() -> Red;
fn turn_off(self);
}
在继续之前,快速回顾一下
- 模块使用
#[typestate]
进行标注,从而启用领域特定语言。- 要声明主自动机,我们将
#[automaton]
附加到一个结构体上。- 状态通过附加
#[state]
进行声明。- 状态函数通过具有相同名称的特质声明。
- 初始和最终状态通过具有“特殊”签名的函数声明。
最后,我们需要解决状态之间如何相互转换的问题。一个敏锐的读者可能会推断,我们可以消费一个状态并返回另一个状态,这样的读者将完全正确。
例如,要在Red
状态和Green
状态之间进行转换,我们这样做
trait Red {
fn to_green(self) -> Green;
}
在此基础上,我们可以完成其他状态的转换
pub trait Green {
fn to_yellow(self) -> Yellow;
}
pub trait Yellow {
fn to_red(self) -> Red;
}
pub trait Red {
fn to_green(self) -> Green;
fn turn_on() -> Red;
fn turn_off(self);
}
完整的代码如下
#[typestate]
mod traffic_light {
#[automaton]
pub struct TrafficLight {
pub cycles: u64,
}
#[state] pub struct Green;
#[state] pub struct Yellow;
#[state] pub struct Red;
pub trait Green {
fn to_yellow(self) -> Yellow;
}
pub trait Yellow {
fn to_red(self) -> Red;
}
pub trait Red {
fn to_green(self) -> Green;
fn turn_on() -> Red;
fn turn_off(self);
}
}
上面的代码将生成
- 将主结构体扩展为具有
state: State
字段。 - 一个密封特质,不允许从外部添加状态。
- 为每个状态提供描述性函数的特质。
高级指南
当描述类型状态时,有一些功能可能会有所帮助。有两个主要功能尚未讨论。
自转换函数
简单来说,状态可能需要在不转换的情况下自我突变,或者我们可能需要一个简单的获取器。为了声明这些目的的方法,我们可以使用接收self
引用(可变或不可变)的函数。
考虑以下示例,其中我们有一个可以立或不立的标志。我们有两个函数,一个检查标志是否立,另一个设置标志立。
#[state] struct Flag {
up: bool
}
impl Flag {
fn is_up(&self) -> bool;
fn set_up(&mut self);
}
由于这些函数没有改变类型状态状态,它们将转换回当前状态。
非确定性转换
考虑一个状态类型依赖于可能失败的外部组件,为了建模这种情况,可以使用 Result<T>
。然而,我们需要让我们的状态类型在已知状态之间进行转换,因此我们需要声明两件事情。
- 声明一个与其它状态并列的
Error
状态。 - 声明一个
enum
来表示状态的分支。
#[state] struct Error {
message: String
}
enum OperationResult {
State, Error
}
在枚举中只能有其他有效状态,并且只支持 Unit
风格的变体。
属性
以下是可用于与 #[typestate]
一起使用的属性列表。
#[typestate]
:主要的属性宏,没有属性参数。#[typestate(enumerate = "...")]
:这个选项让宏生成一个额外的enum
,该枚举允许以“泛型”到状态的方式处理变量和结构。- 参数可以声明为带有或没有字符串字面量,如果声明为带有字符串,则该字符串将用作
enum
的标识符。 - 如果参数使用空字符串或没有字符串,则默认行为是在字符串前添加一个
E
- 参数可以声明为带有或没有字符串字面量,如果声明为带有字符串,则该字符串将用作
#[typestate(state_constructors = "...")
:这个选项为具有字段的州生成基本构造函数。
功能
您可以启用的 cargo 功能
mermaid-docs
将生成嵌入在您的文档中的 Mermaid.js 状态图。此功能默认启用。export-dot
将生成您的状态机的.dot
文件。- 此功能可以通过以下环境变量进行自定义(来自 DOT 文档)
DOT_PAD
- 指定在图形绘制区域周围延伸多少英寸,以扩展所需的最小绘制区域。DOT_NODESEP
- 在dot
中,nodesep
指定相同等级中相邻节点之间的最小空间,以英寸为单位。DOT_RANKSEP
- 在dot
中,设置所需的等级分离,以英寸为单位。EXPORT_FOLDER
- 声明导出文件的目标文件夹。
- 此功能可以通过以下环境变量进行自定义(来自 DOT 文档)
export-plantuml
将生成您的状态机的 PlantUML 状态图(.uml
文件)。- 此功能可以通过以下环境变量进行自定义(来自 PlantUML Hitchhiker's Guide)
PLANTUML_NODESEP
-nodesep
指定了同一层中相邻节点之间的最小空间。PLANTUML_RANKSEP
- 设置所需的层间距。EXPORT_FOLDER
- 声明导出文件的目标文件夹。
- 此功能可以通过以下环境变量进行自定义(来自 PlantUML Hitchhiker's Guide)
类型状态可视化
灯泡 |
智能灯泡 |
|
---|---|---|
DOT | ||
PlantUML |
出版物
依赖关系
~1.5–2.2MB
~51K SLoC