备忘录模式的核心是隔离状态访问权限,而非单纯保存状态;原发器通过private嵌套类Memento封装只读状态,Caretaker仅持有引用而无法读写其内容。

备忘录模式的核心不是“存状态”,而是“隔离状态访问权限”
直接用 public 字段或属性暴露内部状态,再让外部类去序列化、复制——这不是备忘录模式,这是裸奔。真正的关键在于:原发器(Originator)必须能创建和恢复状态,但备忘录(Memento)对象对外部(比如管理者 Caretaker)不可修改,甚至不能读取具体字段。C# 里最自然的实现方式是利用 private 嵌套类 + 只读属性,而非靠文档约定或注释提醒。
用 private 嵌套类 + 只读属性是最稳妥的写法
这样能靠编译器强制保证封装性,连反射都很难绕过(除非用 BindingFlags.NonPublic 主动硬闯)。常见错误是把 Memento 声明为顶层 public 类,或者用 struct 存状态导致值拷贝语义混乱。
-
Memento必须是原发器的private嵌套类,只暴露只读属性(如public int Value { get; }),不提供 setter 或公开字段 - 原发器的
CreateMemento()返回Memento实例,SetMemento(Memento m)接收并还原——这两个方法都只能在原发器内部调用 - Caretaker 只负责持有
Memento引用,完全不知道它内部结构;哪怕你加个ToString(),也只该返回占位符(如"Memento@1a2b3c"),不该泄露数据
public class Editor
{
private string _content = "";
private int _cursorPos;
<pre class="brush:php;toolbar:false;">public void Type(string text) => _content += text;
public Memento CreateMemento() => new Memento(_content, _cursorPos);
public void Restore(Memento m) {
_content = m.Content;
_cursorPos = m.CursorPos;
}
private class Memento
{
public string Content { get; }
public int CursorPos { get; }
public Memento(string content, int cursorPos)
{
Content = content;
CursorPos = cursorPos;
}
}
}
别用 JSON 序列化代替备忘录模式
有人图省事,直接对整个对象做 JsonSerializer.Serialize() 存字符串,恢复时反序列化——这看似“保存了状态”,但破坏了模式意图:它让 Caretaker 实际上获得了读写原始字段的能力(只要改 JSON 字符串就行),也丧失了细粒度控制(比如只想存内容不存光标位置)。更严重的是,一旦原发器字段增减或类型变更,旧快照大概率无法反序列化,抛出 JsonException。
- 序列化适合持久化或跨进程传递,不适合内存内、强契约的撤销/重做场景
- 如果真要用序列化,至少把原发器标记为
[Serializable]并自定义GetObjectData,但依然不如嵌套类清晰可控 - 性能上,深拷贝对象比构造一个轻量
Memento实例慢得多,尤其对象含集合或引用链时
多级撤销时 Caretaker 要管理栈,且注意内存泄漏风险
很多人只实现单个 Memento,但实际需要 Undo/Redo 时,得用 Stack 和 Stack 配合。容易被忽略的是:如果原发器状态很大(比如编辑百万行文本),每个 Memento 都持有完整副本,栈不断增长会吃光内存。
- 建议在 Caretaker 中限制栈大小(如最多存 50 个),超出后自动
.Pop()丢弃最早的一个 - 避免在
Memento中存储可变引用类型(如List),应做浅拷贝或转为不可变集合(IImmutableList) - 如果状态实在太大,考虑用“差异快照”(delta snapshot):只存和上一版的差异,但实现复杂度陡增,一般项目没必要
嵌套类方案看着简单,但权限边界和生命周期控制才是它真正难写对的地方——少一个 private,就等于没用。










