C#中状态模式需三要素对齐:State接口按动作命名方法、Context管控切换入口与校验、ConcreteState禁止反向调ChangeState();否则易退化为if-else搬家或引发循环调用。

直接说结论:C#里状态模式不是“写一堆State类就完事”,而是三件事必须对齐——State接口方法名要和真实触发动作一致、Context必须管控切换入口与校验、ConcreteState绝不能自己调ChangeState()。
State接口别只定义Handle(),按动作拆方法名
常见错误是把所有行为塞进一个Handle()或Execute(),结果每个实现里又写一堆if (input == X),等于把if-else从Context搬进了State里,没解决问题。
正确做法是让接口方法名直接对应外部可触发的动作,比如:
-
InsertCoin()、EjectCoin()、TurnCrank()(糖果机) -
OnEnter()、OnUpdate()、OnExit()(Unity帧循环场景) -
StartProcessing()、CancelProcessing()、CompleteProcessing()(订单工作流)
这样做的好处是:接口语义清晰,IDE能自动补全,新增状态时只需实现对应方法,不会漏掉某类操作;更重要的是,Context在调用前就知道该传什么参数、期待什么返回值。
Context类必须控制状态切换的入口和校验
Context不是状态容器,它是调度员。如果允许外部直接赋值_currentState = new SoldState(),就会绕过合法性检查,导致“正在保存中”直接切到“已发布”,数据不一致。
必须提供明确的切换入口,并内置校验逻辑:
- 用
ChangeState(IState newState)替代公开的Statesetter - 切换前检查是否允许:
if (!_allowedTransitions.Contains((CurrentState.GetType(), newState.GetType()))) throw new InvalidOperationException(...); - 每次调用状态方法前加空引用检查:
if (_currentState != null) _currentState.OnUpdate(this);,避免NullReferenceException - 状态实例不要在Context里
new,应通过构造函数注入或工厂获取,否则无法DI、无法复用、无法Mock
ConcreteState类不能反向调用ChangeState()
这是最隐蔽也最危险的坑。很多初学者在HasQuarterState.TurnCrank()里直接写context.ChangeState(new SoldState()),看似简洁,实则破坏了职责边界:
- 状态类开始感知其他状态的存在,违反“只关注自身行为”原则
- 容易引发循环调用(比如
SoldState.OnEnter()又触发ChangeState()) - 状态转换逻辑分散在各个ConcreteState里,无法统一审计或加日志
正确做法是让状态类通过返回值或事件通知Context:“我处理完了,下一步建议切到X”:
- 返回
Type或IState实例(需Context做类型映射) - 触发
OnStateTransitionRequested事件,由Context订阅并执行切换 - 如果状态无副作用且纯响应式(如
DisplayOnlyState),甚至可以不传Context参数
什么时候不该用状态模式?先看这四条硬指标
状态模式不是银弹。如果出现以下任一情况,用枚举+条件分支更轻量、更可控:
- 状态数 ≤ 3,且各状态行为差异小(比如只是UI颜色/按钮禁用)
- 新增状态平均要改 ≥ 4 个已有方法,说明当前设计已耦合严重,该重构而非套模式
- 状态转换无前置约束(任意状态都能切到任意状态)
- 单个状态类代码
真正值得上状态模式的,是那些状态间有严格流转规则、行为差异大、且日志/监控/异常策略都不同的场景——比如协议解析器、订单生命周期、设备固件状态机。这时候,多出来的几层抽象,换来的是可测试性、可追溯性和长期维护成本的下降。


















