你是否曾在尝试自己实现撤销功能时卡住?
这是按一次 Ctrl+Z 就能完成的常见功能,但真正写进代码后却相当棘手。即使是类似画图软件的玩具项目,也可能让你花上好几天。
先说结论:撤销的实现大致分为两种思路。
一种是通过 Command 模式反向撤销已执行的操作,另一种是通过 Memento 模式完整保存操作前的状态。
两种都正确,只是适用时机不同。今天我们结合代码比较这两种方式。
Command 模式 — 将操作封装为对象
Command 模式会把“做了什么”放进一个对象里。
可以把它理解为一个将 execute 和 undo 配对的对象。
例如,如果操作是输入文字,就提前在同一个对象中定义它的反向操作:“删除输入的内容”。
protocol Command {
func execute()
func undo()
}
struct TypeCommand: Command {
let text: String
let editor: Editor
func execute() { editor.content += text }
func undo() { editor.content.removeLast(text.count) }
}
然后把创建好的 Command 对象逐个压入栈中。
当收到 Ctrl+Z 时,取出栈顶对象并调用 undo() 就完成了。
Command 模式保存的不是状态,而是“操作”。
Memento 模式 — 将状态保存为快照
Memento 采用完全不同的思路。
与其思考如何撤销每个操作,不如直接保存变更前的完整状态,就像拍照一样。
struct Memento { let content: String }
class Editor {
var content = ""
func save() -> Memento { Memento(content: content) }
func restore(_ m: Memento) { content = m.content }
}
let editor = Editor()
editor.content = "你好"
let snapshot = editor.save() // 在这里保存快照
editor.content = "你好"
editor.restore(snapshot)
print(editor.content)
// 输出:你好
需要撤销时,用保存的快照替换当前状态即可。就这么简单。
不需要为每个操作分别编写撤销逻辑,确实省心。
但缺点也很明显。如果状态很大,每次都要完整复制,内存消耗会相当可观。
那么,应该使用哪一种?
我把实际使用两者后感受到的差异整理成了表格。
| 标准 | Command 模式 | Memento 模式 |
|---|---|---|
| 存储对象 | 操作(execute/undo) | 状态快照 |
| 内存 | 轻量 | 状态较大时开销较高 |
| 撤销逻辑 | 针对每个操作手动实现 | 只需恢复状态 |
| redo | 通过出栈自然实现 | 可以使用快照数组 |
| 适合场景 | 编辑器、图形工具 | 游戏存档、表单状态 |
用一句话总结就是:
如果能明确编写撤销操作,就选 Command;如果完整保存状态更省心,就选 Memento。
何时使用,何时避免
实际选择时,我会参考以下标准。
- 操作边界清晰,并且还需要 redo → Command。编辑器和绘图工具就是典型例子。
- 状态较小,复制快照不会造成负担 → Memento。适合表单输入、恢复配置值等场景。
- 状态非常庞大,但改动范围很小 → 避免使用 Memento。每次完整复制都可能耗尽内存,Command 更合适。
- 完全想不出撤销逻辑 → 不要强行使用 Command,用 Memento 保存快照更简洁。
实际工作中也可以混用两者:用 Command 管理操作,只对难以撤销的部分使用 Memento 保存快照。
面试中会这样问
Q. Command 模式和 Memento 模式有什么区别?
Command 将“操作”封装为对象,并将 execute 与 undo 配对。Memento 则把对象的“状态”保存为快照,之后再恢复。也就是说,区别在于通过操作的逆运算撤销,还是通过恢复状态撤销。
Q. 如果要实现撤销栈,你会选择哪一种?
如果操作边界清晰,并且希望自然支持 redo,就选择 Command。相反,如果状态较小,而编写撤销逻辑比较麻烦,用 Memento 堆叠快照会更简单、更安全。
建议你亲自把两种模式各实现一次。只阅读概念,和亲眼看到对象压入栈中、状态通过 Ctrl+Z 回退,理解深度完全不同。希望今天的文章能帮助你迈出第一步。

