7. 状态机嵌套:层次状态机概念、父状态与子状态、状态继承与复用

好,咱们今天聊一个稍微进阶一点的话题——状态机嵌套。说白了,就是状态里面还能再套状态。你可能会问:“有必要搞这么复杂吗?”嗯,我刚开始接触这个概念时也是这个反应。直到有一次我在做一个通信协议栈的项目,状态多到我自己都数不清,画出来的状态图跟蜘蛛网似的……那时候我才意识到,扁平状态机在某些场景下,真的不够用。

7.1 为什么需要层次状态机?

先说说扁平状态机的痛点。假设你正在设计一个智能家居系统,设备有“待机”、“运行”、“告警”三个大状态。每个大状态下,又有若干子状态。比如“运行”状态下,可能还分“加热”、“制冷”、“送风”等子状态。

如果用扁平状态机,你会写出类似这样的状态列表:

// 扁平状态机——状态爆炸
typedef enum {
    STATE_IDLE,
    STATE_HEATING,
    STATE_COOLING,
    STATE_FAN_ONLY,
    STATE_ALARM_HIGH_TEMP,
    STATE_ALARM_LOW_TEMP,
    STATE_ALARM_SENSOR_FAIL,
    // ... 还有更多
} State_t;

你看,光是告警状态就有好几种。而且每个告警子状态里,可能都要处理“退出告警”这个动作。如果每个状态都单独写一遍退出逻辑,代码冗余不说,还容易漏掉。

层次状态机(Hierarchical State Machine, HSM) 就是为了解决这个问题而生的。它允许一个状态包含多个子状态,子状态可以继承父状态的行为。说白了,就是让状态机有了“继承”的能力。

核心思想: 父状态定义通用行为,子状态定义特殊行为。子状态处理不了的事件,交给父状态处理。

7.2 父状态与子状态:谁管谁?

咱们用个具体的例子来说明。假设你正在写一个嵌入式设备的电源管理模块。设备有“正常模式”和“省电模式”两个大状态。每个大状态下,CPU 频率和屏幕亮度都不一样。

先画个图,看看层次关系:

正常模式 (父状态) 全速运行 (子状态) CPU: 1.2GHz, 屏幕: 100% 待机 (子状态) CPU: 800MHz, 屏幕: 50% ... 更多子状态 省电模式 (父状态) 深度睡眠 (子状态) CPU: 200MHz, 屏幕: 关闭 轻度工作 (子状态) CPU: 400MHz, 屏幕: 30%

在这个结构里,“正常模式”是父状态,“全速运行”和“待机”是它的子状态。父状态可以定义一些通用规则,比如“进入正常模式时,打开散热风扇”。子状态只需要关注自己的特殊行为,比如“全速运行”时 CPU 跑满,“待机”时降频。

我的经验: 父状态最适合放“进入/退出”时的公共操作。比如进入“正常模式”时打开风扇,退出时关闭风扇。子状态只管自己特有的逻辑。这样改一个地方,所有子状态都受益。

7.3 状态继承:子状态“继承”父状态的行为

继承这个词,搞过面向对象编程的人应该不陌生。在层次状态机里,子状态会继承父状态的事件处理逻辑。什么意思呢?

举个例子。假设父状态“正常模式”定义了一个事件处理:

  • 收到 EVENT_BATTERY_LOW → 切换到“省电模式”

那么它的子状态“全速运行”和“待机”都会自动继承这个处理逻辑。也就是说,不管当前在哪个子状态,只要电池电量低,都会切换到省电模式。你不需要在每个子状态里重复写这个逻辑。

但子状态也可以“重写”父状态的行为。比如“待机”子状态可能觉得:电池电量低时,我先保存一下数据再切换。那它就可以自己处理 EVENT_BATTERY_LOW,父状态的处理就被覆盖了。

// 层次状态机的事件处理示例
void NormalMode_HandleEvent(Event_t event) {
    switch (event) {
        case EVENT_BATTERY_LOW:
            // 父状态的默认处理:直接切换
            TransitionToState(&PowerSaveMode);
            break;
        case EVENT_TEMP_HIGH:
            // 父状态处理温度过高
            StartFan();
            break;
        default:
            // 未处理的事件,可以忽略或上报
            break;
    }
}

void Standby_HandleEvent(Event_t event) {
    switch (event) {
        case EVENT_BATTERY_LOW:
            // 子状态重写:先保存数据
            SaveUserData();
            // 再调用父状态的处理
            NormalMode_HandleEvent(event);
            break;
        default:
            // 子状态不处理的事件,交给父状态
            NormalMode_HandleEvent(event);
            break;
    }
}

你看,子状态 Standby 处理 EVENT_BATTERY_LOW 时,先做了自己的事(保存数据),然后又调用了父状态的处理。这就是典型的“继承+扩展”。

注意: 子状态重写父状态事件时,一定要想清楚:是“完全替换”还是“扩展”。如果是扩展,记得在最后调用父状态的处理。我曾经因为忘了调用父状态处理,导致一个设备在低电量时没有及时切换模式,差点把电池搞废了。

7.4 状态复用:让代码少写一半

复用是层次状态机的另一个大招。你想想看,如果多个父状态都有相似的子状态结构,是不是可以复用?

比如,不管是“正常模式”还是“省电模式”,可能都有一个“用户交互”子状态。这个子状态负责处理按键、触摸等输入。那我们可以把这个子状态定义成独立的模块,然后在两个父状态里都引用它。

// 可复用的“用户交互”子状态
State_t UserInteractionState = {
    .entry = UserInteraction_Entry,
    .exit = UserInteraction_Exit,
    .handleEvent = UserInteraction_HandleEvent
};

// 正常模式下引用
State_t NormalMode = {
    .subStates = {&FullSpeedState, &StandbyState, &UserInteractionState},
    .entry = NormalMode_Entry,
    .exit = NormalMode_Exit,
    .handleEvent = NormalMode_HandleEvent
};

// 省电模式下也引用同一个子状态
State_t PowerSaveMode = {
    .subStates = {&DeepSleepState, &LightWorkState, &UserInteractionState},
    .entry = PowerSaveMode_Entry,
    .exit = PowerSaveMode_Exit,
    .handleEvent = PowerSaveMode_HandleEvent
};

这样做的好处很明显:

  • 代码量减少:同样的逻辑不用写两遍
  • 维护方便:改一个地方,所有引用它的父状态都生效
  • 一致性:用户交互的行为在任何模式下都一样,用户体验更好

避坑指南: 复用子状态时,要注意子状态内部不能依赖父状态的特定上下文。比如,不要在子状态里直接访问父状态的私有变量。我见过有人把父状态的指针硬编码在子状态里,结果复用时出了大问题。正确的做法是通过事件或回调来传递上下文。

7.5 实现层次状态机的几种方式

说了这么多理论,咱们看看实际怎么实现。我常用的有三种方式:

方式 优点 缺点 适用场景
函数指针 + 状态栈 实现简单,内存可控 嵌套层次深时管理复杂 资源受限的嵌入式系统
状态表 + 父状态指针 结构清晰,易于扩展 需要额外的表结构 中等复杂度的项目
状态机框架(如 QP/C) 功能强大,支持多层嵌套 学习成本高,代码量大 大型复杂系统

我个人比较推荐第二种——状态表 + 父状态指针。它既不会太复杂,又能很好地支持层次结构。下面是一个简单的实现示例:

// 状态表结构
typedef struct State {
    void (*entry)(void);                    // 进入动作
    void (*exit)(void);                     // 退出动作
    void (*handleEvent)(Event_t event);     // 事件处理
    const struct State *parent;             // 父状态指针
} State_t;

// 事件分发函数
void DispatchEvent(Event_t event) {
    const State_t *current = currentState;
    
    // 从当前状态开始,逐层向上查找能处理该事件的状态
    while (current != NULL) {
        if (current->handleEvent != NULL) {
            current->handleEvent(event);
            // 如果事件被处理了,就停止向上传递
            // 这里假设 handleEvent 返回 true 表示已处理
            // 实际项目中可以用返回值或全局标志
        }
        current = current->parent;
    }
}

// 状态切换函数
void TransitionToState(const State_t *target) {
    // 退出当前状态及其父状态
    const State_t *p = currentState;
    while (p != NULL) {
        if (p->exit) p->exit();
        p = p->parent;
    }
    
    // 进入目标状态及其父状态
    // 注意:需要从顶层父状态开始进入
    // 这里简化处理,实际需要记录路径
    currentState = target;
    p = target;
    while (p != NULL) {
        if (p->entry) p->entry();
        p = p->parent;
    }
}

这个实现里,每个状态都有一个 parent 指针指向它的父状态。事件分发时,如果当前状态处理不了,就自动向上找父状态处理。这就是层次状态机的核心机制。

小技巧: 状态切换时,退出和进入的顺序很重要。退出时从当前状态开始,逐层向上退出;进入时从顶层父状态开始,逐层向下进入。这样才能保证状态机的行为符合预期。

7.6 实际项目中的经验总结

最后,分享几个我在项目中积累的经验:

  1. 不要过度嵌套:层次超过3层,代码就很难维护了。我一般控制在2层以内。
  2. 父状态不要做太多事:父状态只放公共逻辑,特殊逻辑交给子状态。否则父状态会变得臃肿。
  3. 小心“状态风暴”:子状态切换时,父状态可能也会跟着切换。要确保切换逻辑是确定的,不会出现死循环。
  4. 善用“历史状态”:有些层次状态机支持“历史状态”,即退出父状态后,再次进入时恢复到之前的子状态。这个功能很实用,但实现起来要小心。

嗯,关于层次状态机,今天就聊这么多。说白了,它就是让状态机有了“继承”和“复用”的能力,让代码更简洁、更易维护。下次你遇到状态多到头疼的项目,不妨试试这个思路。

一句话总结: 父状态管通用,子状态管特殊。事件从下往上找,谁有能力谁处理。


公众号:蓝海资料掘金营,微信deep3321