yield实现撤销重做状态机的核心是将每步建模为可中断、可回溯的生成器步骤,产出执行/撤销函数对或纯数据指令,由外部调度器管理游标与历史。

用 yield 实现撤销重做状态机,核心不是“挂起控制流”,而是把每一步操作建模为可中断、可回溯的生成器步骤,并通过外部调度器统一管理执行与逆向。它不复刻同步命令模式的调用栈,而是用生成器协议(send/throw)显式控制正向/反向流转。
把操作拆成带逆操作的 yield 步骤
每个 yield 点代表一个原子操作及其撤回逻辑。生成器本身不保存状态,只产出“执行函数”和“撤销函数”组成的元组;状态由外部维护。
- 不要在生成器内部修改共享变量,而应 yield 一个描述动作的纯数据结构(如
{op: 'add', item: x}) - 对应地 yield 撤销指令(如
{op: 'remove', id: x.id}),或直接 yield 执行/撤销两个函数 - 例如:
yield lambda: items.append(x), lambda: items.pop()
用生成器对象 + 游标位置实现线性历史
生成器本身不可倒退,但可将整个生成器转为惰性序列,配合整数游标模拟“当前步骤”。每次 next() 向前,用 send(undo=True) 或单独维护撤销栈来触发逆向。
- 启动时用
gen = my_workflow(); next(gen)获取第一步 - 记录每步返回的动作对,存入列表
history = [];游标pos = -1表示初始空状态 - 执行下一步:若
pos ,先撤销后续所有步骤;再 <code>pos += 1,执行history[pos][0]()
避免 yield 混淆控制权,用调度器统管流向
不要让业务逻辑直接调用 next() 或 send(),而应封装一层调度器(如 UndoRedoManager),它持有生成器、历史列表和当前位置,对外只暴露 do()、undo()、redo() 方法。
- 生成器只负责定义“有哪些步骤”,不决定何时走哪步
- 调度器在
do()中驱动生成器前进,并缓存返回的动作对;undo()则调用该步的撤销函数并移动游标 - 这样既保持生成器的声明式表达力,又隔离了控制流复杂度
注意边界:yield 不等于协程,无法自动保存局部变量
生成器暂停时只保留栈帧,但局部变量不会跨 yield 持久化。需要持久化的中间状态(如表单输入、临时计算结果)必须显式传入或从外部读取。
- 错误写法:
tmp = expensive_calc(); yield tmp; print(tmp)——tmp在下次恢复时仍存在,但不可靠依赖 - 正确做法:所有需跨步访问的数据,都通过参数传入生成器,或由调度器统一管理上下文对象
- 若需复杂状态,建议用类封装生成器逻辑,把状态作为实例属性,
yield只负责“通知下一步要做什么”











