状态机在C#中应据场景选switch或StatePattern:状态少、逻辑直白用switch;需行为封装、生命周期管理则用StatePattern;StatePattern适用于状态自身携带行为、可复用、需隔离副作用的场景。

状态机在 C# 里到底该用 switch 还是 StatePattern?
多数人一上来就写一堆 if-else 或 switch 切状态,短期能跑,但加个“暂停中转到错误”的新路径就容易漏掉边界判断。真要稳,得先分清场景:switch 适合状态少、跳转逻辑直白(比如解析器的 token 状态)、且不涉及状态生命周期管理;而 StatePattern 是为「状态自身携带行为+可复用+需隔离副作用」准备的——比如一个网络连接对象,Connected 状态下允许发包,Disconnected 下调用 Send() 就该直接抛异常,而不是靠上层反复 if (state == Connected)。
别硬套设计模式。3 个状态以内、无异步等待、无状态进入/退出钩子,switch 更轻量;一旦出现「某个状态要持有一个定时器」「状态间传递上下文对象」「需要单元测试单个状态行为」,立刻切到 StatePattern。
StateMachine(如 Stateless)为什么上线后总出诡异跳转?
用 Stateless 库时,最常踩的坑不是配置错,而是忽略了触发器(TTrigger)的不可变性。如果传入的是一个可变对象(比如 class NetworkEvent { public string Code; }),并在触发后修改了 Code,后续状态检查可能基于已被污染的值——因为 Stateless 默认按引用缓存触发器实例做匹配。
- 始终用
enum或string做TTrigger,避免自定义类 - 注册转换时显式调用
Configure(State).Permit(Trigger, NextState),别依赖隐式发现 - 在
OnTransitioned回调里打日志,但注意:回调发生在状态已变更之后,想拦截非法跳转得用OnEntry/OnExit+ 抛异常 - 并发调用
Fire()会导致状态撕裂——必须自己加lock或用AsyncLock包一层
async/await 和状态机混用时,Task 生命周期怎么跟状态对齐?
C# 编译器生成的 async 状态机和业务状态机是两套东西,强行绑定会出问题。典型错误是:在 OnEntry 里启动一个 Task,然后以为“等它完成状态才真正就绪”,结果外部代码已经根据新状态执行下一步了。
正确做法只有一条:状态变更必须是同步的,异步动作只能作为状态的副作用,且不能阻塞状态流转。
- 在
OnEntry启动后台任务时,用Task.Run(() => {...}).Forget()(需引用Microsoft.Extensions.DependencyInjection的扩展)或手动捕获异常,但绝不await - 如果某状态必须“等待异步操作完成才算进入成功”,那就把这个状态拆成两个:
Connecting→(异步完成)→Connected,中间用事件或回调驱动第二次Fire() - 避免在
OnExit里await,这会让状态机卡住——改用同步清理资源,异步部分走独立管道
自己手写状态机时,goto 和 while + switch 哪个更可控?
别被“goto有害论”带偏。在极简嵌入式协议解析、高性能报文分发等场景,手写状态机用 goto 反而最干净——没有循环开销,跳转目标明确,JIT 也优化得好。但前提是:状态数固定、无动态加载、不需调试可视化。
示例片段:
ParseLoop:
switch (currentByte)
{
case 0xFF: state = State.Header; goto ParseLoop;
case 0x00: state = State.Payload; goto ParseLoop;
default: goto Error;
}
Error:
// 处理错误
而 while + switch 更易读、支持 break/continue、方便插桩测覆盖率,但每次循环都有条件判断开销。如果状态跳转频繁(比如每微秒处理一个字节),goto 能省下几个 cycle;日常业务代码,选 while + switch 更稳妥——毕竟人比 CPU 容易出错。
真正难的从来不是语法选择,而是状态迁移图有没有画全,特别是所有 error → idle、timeout → retry 的退路有没有覆盖。漏一条,线上就多一个 stuck 状态。










