编辑事务:能撤销不等于操作已经原子
自动关联目录:编辑事务:能撤销不等于操作已经原子
核心判断:Undo 记录只能证明“以后可能恢复”,不能证明一次编辑在失败时没有留下半成品。编辑命令必须先准备、再原子提交,撤销只是提交后的逆操作。
一、业务问题:失败不能把作品留在中间态
一次行为配置修改可能同时改变属性、父子关系、运行时缓存和编辑器选择。若第三步失败,仅丢弃 Undo 记录,前两步已经发生的写入不会自动消失。用户看到的是“操作报错,但内容已经悄悄变了一半”。
工程中的成熟路径已经按“捕获前态、开启批次、准备变更、拒绝空操作、准备应用、写入撤销记录、提交应用、提交批次”执行;任何准备失败都会取消批次。这比“先改 UObject,再补一条历史”可靠,值得保留。
二、真正的事务需要两类原子性
编辑事务至少有两层:
- 模型原子性:对象树、属性和引用要么一起变,要么都不变。
- 副作用原子性:重建组件、刷新 Slate、标脏、通知脚本等只能在模型提交后发布;失败时可重试,不能反向污染模型。
UE 的 FScopedTransaction 更接近历史记录和 UObject 状态恢复工具,不是跨自定义数据、运行时对象与异步副作用的数据库事务。把所有逻辑包进一个作用域,并不会自动获得两阶段提交。
三、目标契约:Prepare 不产生外部可见写入
Result Execute(EditCommand cmd, EditSession session) {
Snapshot before = session.Model().Capture(cmd.ReadSet());
PreparedChange change = cmd.Prepare(before); // 校验、求差、估算成本
if (change.IsNoOp()) return Result::NoChange;
UndoEntry undo = change.BuildInverse(before);
CommitGuard guard(session, change.WriteSet());
change.CommitModel(); // 唯一前向写点
session.History().Push(undo);
guard.CommitRevision();
session.Effects().Publish(change.Effects()); // 可重试的提交后副作用
return Result::Committed;
}若现有模型无法一次交换,可用影子树、变更集或补偿日志实现;关键是 Cancel 必须撤销已经发生的前向写,而不只是删历史记录。
四、取舍、验证与反思
准备阶段会增加快照内存和命令样板代码,大对象操作还要控制单事务字节数。但它换来可测试的失败语义:在每个准备点和提交点注入故障,验证模型哈希、历史深度、Dirty Revision 与运行时投影要么全部前进,要么全部不动。
这次复盘暴露出过去容易问错的问题:不应只问“这个操作能不能 Undo”,而应先问“它有几个前向写点,哪个写点之后失败会留下什么”。
许可协议:CC BY
作者:Davids
本文链接:https://hustjjd.github.io/3c4cfe33.html
更新于:2026年10月14日