状态机基础:有限状态机(FSM)概念、状态、事件、转移、动作的定义

好,咱们正式开始聊状态机。说实话,我最早接触状态机是在做遥控器项目的时候。那时候按键逻辑乱成一锅粥,后来用了状态机,整个世界清静了。你想想看,一个系统如果状态多了,用 if-else 堆下去,迟早把自己绕晕。

有限状态机,英文叫 Finite State Machine,简称 FSM。说白了,就是描述一个系统在不同“状态”之间怎么跳来跳去的模型。它有三个核心要素:状态、事件、转移。再加上动作,一共四个东西。咱们一个一个拆开讲。

1. 状态(State)

状态是什么?就是系统在某个时刻的“模样”。比如飞行模式,要么是“开启”,要么是“关闭”。这就是两个状态。我习惯用枚举来定义状态,清晰明了。

状态的特点:

  • 互斥性:同一时刻只能处于一个状态。飞机不可能既在起飞又在巡航。
  • 稳定性:没有事件触发时,状态保持不变。不会自己乱跳。
  • 可数性:状态数量是有限的。这就是“有限”二字的含义。

我在项目中遇到过一个问题:有人把状态定义得太细,比如“待机-等待按键-等待确认”,结果状态数量爆炸。我的建议是,状态粒度要适中,别把临时条件也当成状态。

2. 事件(Event)

事件就是“发生了什么”。它是状态转移的触发器。比如用户按了一下飞行模式开关,这就是一个事件。事件可以是外部的(按键、传感器信号),也可以是内部的(定时器超时、数据接收完成)。

嗯,这里要注意:事件本身不改变状态,它只是告诉状态机“该看看要不要转移了”。真正改变状态的是转移逻辑。

我的经验:事件定义要尽量原子化。别把“长按3秒”定义成一个事件,而是拆成“按键按下”和“定时器3秒超时”两个事件。这样组合起来更灵活。

3. 转移(Transition)

转移就是“从状态A到状态B的路径”。它由三部分组成:

  • 源状态:当前在哪儿
  • 触发事件:发生了什么
  • 目标状态:要去哪儿

举个例子:飞行模式关闭状态下,收到“用户按下开关”事件,转移到飞行模式开启状态。这就是一条完整的转移。

我曾经犯过一个低级错误:忘了处理“同一事件在不同状态下的不同行为”。比如在飞行模式开启时再按开关,应该关闭;在关闭时按开关,应该开启。这其实是两条不同的转移,别搞混了。

4. 动作(Action)

动作是转移发生时“要做的事”。比如进入飞行模式时,需要关闭Wi-Fi、蓝牙、蜂窝网络。这些操作就是动作。

动作可以发生在三个时机:

  • 进入动作:进入某个状态时执行
  • 退出动作:离开某个状态时执行
  • 转移动作:转移过程中执行

避坑指南:我曾经在转移动作里写了阻塞代码,结果状态机卡住了。记住,动作要尽量短小精悍,别在里面做耗时操作。如果必须做,考虑用异步方式。

知识体系总览

下面这张图,是我自己画的状态机核心逻辑。你看一眼,基本就全明白了。

状态 A (飞行模式关闭) 状态 B (飞行模式开启) 事件:按下开关 动作:关闭无线 事件:再次按下 动作:恢复无线 图例说明: 状态(State) 转移(Transition) 事件 + 动作

状态机的基本模型

一个标准的FSM可以用数学方式表示:FSM = (S, E, T, A, s0)。其中:

符号 含义 举例
S 有限状态集合 {关闭, 开启}
E 有限事件集合 {按下开关, 定时超时}
T 转移函数:S × E → S (关闭, 按下开关) → 开启
A 动作集合 {关闭Wi-Fi, 开启Wi-Fi}
s0 初始状态 关闭

说白了,状态机就是一张“地图”。状态是地点,事件是路标,转移是路线,动作是沿途要做的事。你只要把这张图画清楚,代码写起来就是水到渠成的事。

代码示例:飞行模式状态机

下面是一个简单的C语言实现。我故意写得简洁,方便你看清结构。

// 状态定义
typedef enum {
    FLIGHT_MODE_OFF,
    FLIGHT_MODE_ON
} FlightState;

// 事件定义
typedef enum {
    EVENT_TOGGLE_SWITCH,
    EVENT_TIMEOUT
} FlightEvent;

// 状态机结构
typedef struct {
    FlightState currentState;
    void (*entryAction)(FlightState);
    void (*exitAction)(FlightState);
} FlightFSM;

// 转移表
FlightState transitionTable[2][2] = {
    // 当前状态: OFF
    { FLIGHT_MODE_ON,  FLIGHT_MODE_OFF },  // EVENT_TOGGLE_SWITCH -> ON, EVENT_TIMEOUT -> OFF
    // 当前状态: ON
    { FLIGHT_MODE_OFF, FLIGHT_MODE_ON }    // EVENT_TOGGLE_SWITCH -> OFF, EVENT_TIMEOUT -> ON
};

// 状态机处理函数
void handleEvent(FlightFSM *fsm, FlightEvent event) {
    FlightState nextState = transitionTable[fsm->currentState][event];
    if (nextState != fsm->currentState) {
        // 执行退出动作
        if (fsm->exitAction) fsm->exitAction(fsm->currentState);
        // 更新状态
        fsm->currentState = nextState;
        // 执行进入动作
        if (fsm->entryAction) fsm->entryAction(fsm->currentState);
    }
}

我的习惯:用二维数组做转移表,代码量少,而且容易查错。你只要保证行和列对应好状态和事件,基本不会出bug。我曾经用switch-case写过,后来发现状态一多,代码就变得又臭又长。

总结一下

状态机不是什么高深的东西。它就是一套“规则”,告诉系统在什么情况下该做什么。状态、事件、转移、动作,这四个概念你记住了,后面学什么嵌套状态机、层次状态机,都是在这个基础上加花样。

嗯,最后说一句:别把状态机想得太复杂。你每天用的手机、坐的电梯,里面全是状态机。它就在你身边。


专注资料整理