7 个版本
0.5.5 | 2021 年 5 月 21 日 |
---|---|
0.5.4 | 2021 年 5 月 14 日 |
0.5.3 | 2021 年 4 月 18 日 |
0.4.0 | 2021 年 4 月 17 日 |
#804 在 Rust 模式 中
每月 23 次下载
29KB
241 行
Apparat
轻量级的事件驱动行为状态机
显著特性
- 无 unsafe,除非您主动启用它(见下文“特性标志”)
- 与 no-std 兼容
- 小巧且编译速度快
- 少于 250 行代码
- 无依赖项
- 无过程宏
- 尽管宏的数量可管理,但非常易于使用
- 高度灵活,适用于多种用例
- 无动态分发,使编译器优化成为可能
注意: 我仍在对此进行一些实验,因此版本低于 1.0 时,在点版本发布中可能会有破坏性 API 变更。如果您想确保这一点,请在 Cargo.toml 中指定确切的版本号。在点-点版本发布中永远不会进行破坏性更改。
特性标志
"unsafe"(默认禁用)
此特性标志在某些情况下可以促进更多的编译器优化(使用 cargo asm 验证)。这是通过在一处使用 core::hint::unreachable_unchecked()
来实现的,作者确信它是可靠的。尽管如此,使用此功能风险自负。如果您对此感兴趣,请查看代码 - 我已在其中写了一个关于我的假设的安全性注释。如果您认为我遗漏了什么,请提交一个问题。
架构和用法
您提供的类型
- 任意数量的类型,代表不同的状态*
- 一个上下文类型,在处理事件和转换期间可变访问。
- 一个事件类型,状态机可以处理
- 一个返回类型,每当处理一个事件时都会返回。如果不需要,可以使用单元类型。
* 状态类型内的数据仅在该状态内可访问。这些数据在转换时会被丢弃,在处理事件时会移动。在某些情况下,这些移动可能会被优化掉,但一般来说,为了获得最佳性能,状态类型应该相对较小且易于移动。
如果无法保持它们较小,并且您在未进行转换的情况下处理了大量事件,请考虑在状态类型中将更大的数据放在一个Box
中。或者,您也可以将更大的数据作为上下文类型的一部分,这样就不会被移动或丢弃。但如果是后者,数据当然也会从其他状态中可访问。
注意:所有提供的类型都必须实现Debug
。
生成的实体
- 一个包装枚举,具有所有提供的状态类型的变体
- 为包装枚举实现
ApparatWrapper
特质 - 为包装枚举实现
ApparatState
特质。枚举将所有该特质的调用委托给内部状态对象。 - 为所有提供的状态实现
Wrap
特质(方便起见)
特质
ApparatState<StateWrapper>
必须为所有状态类型实现此特质。唯一要求的方法是handle
,它接受一个事件并返回一个Handled<StateWrapper>
。这仅仅是下一个状态包裹在StateWrapper
中,并附带一个输出值。为了构造此返回类型,我们首先可以在我们的状态上调用.wrap()
,然后在包装器上调用.with_output(...)
。如果我们的输出类型实现了Default
,我们也可以使用.default_output()
。
在ApparatState
特质中还有另外两个方法:init
和is_init
。这些构成了一个初始化机制:在构造一个Apparat
之后,在处理任何事件之后,都会在当前状态上调用init
,直到is_init
返回true
。这样,单个事件可以触发多个、上下文相关的转换。这种情况在一个循环中发生,而不涉及递归。如果一个状态不需要这样,这些方法可以忽略。
TransitionFrom<OtherState, ContextData>
可以使用TransitionFrom
特质来定义状态之间的特定转换。然后自动实现TransitionTo
特质,因此我们可以使用 turbofish 语法调用.transition
。这种设计类似于标准库中的From
和Into
,但TransitionFrom
和TransitionInto
还可以作为副作用修改提供的上下文。TransitionInto
建议用于特质界限。
如果您需要使用相同的状态构造方法定义许多转换,可以使用 transitions
宏。在宏调用内部,可以像这样定义一个转换:StateA -> StateB::new
。在这种情况下,类型 StateB
必须实现一个具有以下签名的 new
方法:fn new(prev: StateA, ctx: &mut ContextData) -> Self
。如果从多个不同的状态到 StateA
的转换需要使用相同的转换方法,则第一个参数必须是泛型的。所有这些都在名为 "counter_transitions_macro" 的示例中得到了演示。
包装<StateWrapper>
Wrap<StateWrapper>
trait 提供了一个 wrap
方法,将单个状态对象转换为 StateWrapper
。与使用 into
相比,这更简洁,并且在更多情况下允许类型推断。宏会自动为所有状态类型实现 Wrap
。
示例
要查看一个更完整的示例,请查看 examples 目录中的 counter.rs。
//! This state machine switches from `StateA` to `StateB` on a single event but
//! then needs three events to switch back to `StateA`. Additionally it keeps
//! track of how often it got toggled back from `StateB` to `StateA`.
use apparat::prelude::*;
// Define the necessary types
// --------------------------
// States
#[derive(Debug, Default)]
pub struct StateA;
#[derive(Debug, Default)]
pub struct StateB {
ignored_events: usize,
}
// Context
// Data that survives state transitions and can be accessed in all states
#[derive(Debug, Default)]
pub struct ContextData {
toggled: usize,
}
// Auto-generate the state wrapper and auto-implement traits
// ---------------------------------------------------------
// In this example we are just using the unit type for `event` and `output`
// because we are only handling one kind of event and we don't care about values
// being returned when events are handled.
build_wrapper! {
states: [StateA, StateB],
wrapper: MyStateWrapper, // This is just an identifier we can pick
context: ContextData,
event: (),
output: (),
}
// Define transitions
// ------------------
impl TransitionFrom<StateB> for StateA {
fn transition_from(_prev: StateB, ctx: &mut ContextData) -> Self {
// Increase toggled value
ctx.toggled += 1;
println!("B -> A | toggled: {}", ctx.toggled);
StateA::default()
}
}
impl TransitionFrom<StateA> for StateB {
fn transition_from(_prev: StateA, ctx: &mut ContextData) -> Self {
println!("A -> B | toggled: {}", ctx.toggled);
StateB::default()
}
}
// Implement the `ApparatState` trait for all states
// -------------------------------------------------
impl ApparatState for StateA {
type Wrapper = MyStateWrapper;
fn handle(self, _event: (), ctx: &mut ContextData) -> Handled<MyStateWrapper> {
println!("A handles event | toggled: {}", ctx.toggled);
// Transition to `StateB`
let state_b = self.transition::<StateB>(ctx);
// Now we need to wrap that `state_b` in a `MyStateWrapper`...
let state_b_wrapped = state_b.wrap();
// ... and add an output value to turn it into a `Handled<...>`.
state_b_wrapped.default_output()
// If we would need a different output value or our output type wouldn't
// implement `Default` we would have to use `.with_output(...)` instead.
}
}
impl ApparatState for StateB {
type Wrapper = MyStateWrapper;
fn handle(mut self, _event: (), ctx: &mut ContextData) -> Handled<MyStateWrapper> {
println!("B handles event | toggled: {}", ctx.toggled);
if self.ignored_events == 2 {
self.transition::<StateA>(ctx).wrap().default_output()
} else {
self.ignored_events += 1;
self.wrap().default_output()
}
}
}
// Run the machine
// ---------------
fn main() {
let mut apparat = Apparat::new(StateA::default().wrap(), ContextData::default());
// Handle some events
for _ in 0..10 {
apparat.handle(());
}
}