命令对象必须保存执行前的完整状态以支持撤销,历史栈用list管理并清空重做栈,光标移动等操作需记录原始位置,撤销时须绕过命令链防止循环入栈。

命令对象必须保存反向操作所需的所有状态
撤销不是简单地调用 undo() 就能回退——它依赖命令对象自己记住“执行前的样子”。比如编辑器中 InsertCommand 插入文字后,undo() 要删掉刚插的那段,就必须在 execute() 时把插入位置和内容都存下来,不能只记当前光标位置。
常见错误是只保存“变化结果”,比如只存 self.text = new_text,导致撤销时无法还原旧状态。正确做法是:在 __init__ 或 execute() 中显式捕获上下文:
class DeleteCommand:
def __init__(self, editor, start, length):
self.editor = editor
self.start = start
self.length = length
# 执行前快照:关键!必须在此刻读,不能等 undo 时再读
self.deleted_text = editor.get_text()[start:start+length]
<pre class='brush:python;toolbar:false;'>def execute(self):
self.editor.delete(self.start, self.length)
def undo(self):
self.editor.insert(self.start, self.deleted_text)</pre>命令历史栈要用 list 而非 stack 类型结构
Python 的 list 支持 append() 和 pop(),天然适合作为命令栈,但要注意:撤销后新操作必须清空“重做栈”,否则会破坏线性时序。很多实现漏掉这步,导致按 Ctrl+Z 再 Ctrl+Y 时行为错乱。
典型结构如下:
立即学习“Python免费学习笔记(深入)”;
-
self.history = []:已执行命令(可撤销) -
self.redo_stack = []:被撤销的命令(可重做) - 每次
execute(command)后:self.history.append(command),同时self.redo_stack.clear() - 每次
undo()后:cmd = self.history.pop(),然后self.redo_stack.append(cmd)
别用 collections.deque 替代 list——除非你真需要 O(1) 头部操作,而命令栈只在尾部增删,list 更直观、内存更紧凑。
文本编辑器的命令需区分“有状态”与“无状态”操作
像 MoveCursorCommand 这类纯位移操作,如果只记录目标位置,多次连续移动后撤销会跳回最初位置,而非上一次位置——这是用户直觉不符的。解决办法是让所有命令都基于相对偏移或绝对坐标,且统一以“执行前状态”为基准。
更稳妥的做法是:把光标移动也视为“影响编辑状态”的操作,哪怕没改文本。例如:
class MoveCursorCommand:
def __init__(self, editor, delta):
self.editor = editor
self.delta = delta
# 记录执行前位置,不是目标位置
self.old_pos = editor.cursor_pos
<pre class='brush:python;toolbar:false;'>def execute(self):
self.editor.move_cursor(self.delta)
def undo(self):
# 回到 old_pos,不是 old_pos - delta
self.editor.set_cursor(self.old_pos)</pre>注意:如果用户快速连按方向键,每个 MoveCursorCommand 都应独立入栈,不能合并——否则撤销时会跳过中间态。
撤销操作不能触发新命令入栈
这是最容易被忽略的陷阱:当用户点击“撤销”,undo() 方法本身又调用了编辑器的 insert() 或 delete(),这些方法若仍走命令模式流程,就会把 undo 动作再次压入 history,造成无限循环或栈污染。
解决方案只有两个字:绕过。要么给编辑器加个 _raw_insert() 类似私有方法,不走命令链;要么在命令执行器里加标志位:
class CommandInvoker:
def __init__(self):
self.history = []
self.redo_stack = []
self._is_undoing = False # 关键开关
<pre class='brush:python;toolbar:false;'>def execute(self, command):
if not self._is_undoing:
self.history.append(command)
self.redo_stack.clear()
command.execute()
def undo(self):
if not self.history:
return
self._is_undoing = True
cmd = self.history.pop()
cmd.undo()
self.redo_stack.append(cmd)
self._is_undoing = False</pre>没有这个标志位,cmd.undo() 内部调用的 editor.insert() 又会触发新 InsertCommand 入栈,撤销就永远撤不完。


















