备忘录模式必须用Originator的private嵌套类实现Memento,确保状态封装与不可变性;所有属性只读、构造参数只读、CreateMemento与SetMemento成对私有、Caretaker仅持引用不访问字段。

备忘录模式不是“把对象存下来就行”,而是用编译器强制隔离状态访问权限;Memento必须是Originator的private嵌套类,否则就只是浅拷贝+暴露字段,根本不算模式。
为什么不能把 Memento 声明为 public 类
一旦Memento是public顶层类,Caretaker 就能直接 new 它、改它的字段、甚至继承它——这等于把原发器的私有状态全摊开在阳光下。更危险的是,有人会顺手加个public string Content { get; set; },结果 Undo 时恢复的是被篡改过的快照。
实操建议:
-
Memento只能是Originator内部的private class,连internal都不够安全(同一程序集其他类仍可能反射绕过) - 所有属性必须是只读的:
public string Content { get; },不提供set,也不暴露public字段 - 构造函数参数全部用
readonly语义接收,避免在构造中意外修改传入引用(比如传入List<T>时应做深拷贝)
CreateMemento 和 SetMemento 必须成对出现在 Originator 内部
这两个方法是状态进出的唯一闸门。如果CreateMemento()在Originator里,而SetMemento(Memento m)被挪到Caretaker里调用,那就破坏了“宽接口仅对原发器开放”的契约——Caretaker此时已经能决定怎么还原,也就隐式获得了读取和解释Memento结构的能力。
常见错误现象:
- 还原后内容错乱,比如光标位置没变、但文本丢了——因为
SetMemento没校验m.Content是否为null,也没处理空字符串边界 - Undo 多次后崩溃,抛出
NullReferenceException——因为SetMemento直接赋值字段,没封装成私有恢复逻辑,导致部分字段未初始化
正确写法:把状态还原逻辑收进私有方法,例如private void RestoreFrom(Memento m),再由SetMemento统一调用。
Caretaker 只能持引用,绝不能碰 Memento 的任何字段
哪怕只是写个Console.WriteLine(m.ToString()),也得确保Memento.ToString()返回的是占位符(如"Memento@4a5d8b2"),而不是拼接Content和CursorPos。否则日志一打,快照内容就泄露了。
使用场景与性能影响:
- 多步 Undo/Redo 必须用两个栈:
private readonly Stack<Memento> _undoStack = new();和private readonly Stack<Memento> _redoStack = new(); - 每次修改前调用
_undoStack.Push(originator.CreateMemento()),同时_redoStack.Clear()——这是防止“撤销后又输入,再重做却回到老状态”的关键 - 如果
Originator状态很大(比如含Bitmap或大数组),别在Memento里存原始引用,要用new byte[oldArray.Length]做深拷贝,否则多个快照共享同一内存,改一个全崩
别用 JsonSerializer.Serialize 代替 Memento
用JsonSerializer.Serialize(originator)看似一行搞定,实际埋了三颗雷:第一,旧快照在字段增减后直接反序列化失败,抛JsonException;第二,Caretaker能手动编辑 JSON 字符串篡改状态;第三,DateTime、enum等类型序列化行为依赖全局配置,换环境就错。
实操建议:
- 显式定义快照结构,例如
public record EditorState(string Content, int CursorPos, bool IsBold); -
CreateMemento()只提取必要字段构造这个 record,不碰Originator的私有实现细节 - record 自带不可变性与值语义,比 class + 只读属性更省心,且编译器能帮你挡掉大部分误操作
真正难的不是写几行代码存状态,而是让团队里任何一个新成员接手时,都**无法绕过封装机制去读写快照**——这得靠嵌套类的访问修饰符、只读属性、以及 record 的不可变性共同守住,少一个环节,模式就塌一半。


















