你是否曾在编码或编辑文档时,手滑误删了关键内容,然后疯狂按下 Ctrl+Z 却发现无法挽回?在软件开发中,为用户提供撤销/重做(Undo/Redo)功能就像给应用装上了一台“时光机”。今天要介绍的 undoredo 库,正是为 Rust 生态量身定做的这样一款利器。
为什么我们需要撤销/重做?
设想你正在构建一个图形编辑器。用户画了一个多边形,再画一个,然后想删除第一个。如果没有撤销功能,误操作将带来极差的体验。软件开发中,实现撤销/重做主要有几种经典设计模式:
- 命令模式(Command Pattern):将每个操作封装为命令对象。
- 备忘录模式(Memento Pattern):保存状态的完整快照。
- 增量记录(Delta Recording):只记录发生变化的部分。
传统的命令模式虽然灵活,但实现起来颇为繁琐。你需要为每个操作定义命令类,处理其执行、撤销和重做逻辑。而 undoredo 库采用了一种更优雅的方式:自动记录增量变化。它像一个默默记录的私人助理,精准记下每一步修改,让你随时能“回到过去”。
快速上手:三分钟玩转 undoredo
安装依赖
首先,在你的 Cargo.toml 中添加以下依赖:
[dependencies]
undoredo = { version = "0.9.13", features = ["derive"] }
注意,features = ["derive"] 仅在需要为自定义结构体启用增量记录时才须添加。若仅使用标准库的集合类型,则不必加。
第一个例子:用 HashMap 体验撤销重做
我们先通过一个简单的 HashMap 示例看看效果:
use std::collections::HashMap;
use undoredo::{Delta, Recorder, UndoRedo};
fn main() {
// 创建一个记录器,它会记录对HashMap的所有修改
let mut recorder: Recorder<HashMap<usize, char>> = Recorder::new(HashMap::new());
// 创建一个撤销/重做管理器
let mut undoredo: UndoRedo<Delta<HashMap<usize, char>>> = UndoRedo::new();
// 插入一些元素
recorder.insert(1, 'A');
recorder.insert(2, 'B');
recorder.insert(3, 'C');
// 提交修改记录
undoredo.commit(&mut recorder);
// 检查一下,元素都在
assert!(*recorder.container() == HashMap::from([(1, 'A'), (2, 'B'), (3, 'C')]));
// 撤销操作
undoredo.undo(&mut recorder);
// 现在HashMap空了
assert!(*recorder.container() == HashMap::from([]));
// 重做操作
undoredo.redo(&mut recorder);
// 元素又回来了
assert!(*recorder.container() == HashMap::from([(1, 'A'), (2, 'B'), (3, 'C')]));
// 用完记录器后,可以释放它,拿回HashMap的所有权
let (hashmap, ..) = recorder.dissolve();
assert!(hashmap == HashMap::from([(1, 'A'), (2, 'B'), (3, 'C')]));
}
逻辑很清晰:Recorder 负责见证并记录修改,UndoRedo 则管理历史状态。你只需调用 commit、undo 和 redo 这几个方法,便实现了完整的撤销重做功能。
原理揭秘:增量记录是如何工作的?
undoredo 的精妙之处在于只记录变化。假设 HashMap 初始为空,当我们插入 (1, 'A') 和 (2, 'B') 时,Recorder 会分别记下这两个操作。
调用 commit 时,这些记录被打包成一个“增量”(Delta),它包含的是修改的详细描述,而非整个 HashMap 的副本。执行 undo 时,库会取出这个增量并反向应用,即删除键1和键2的值,使 HashMap 回到空状态。redo 则再次正向应用增量。
这种方式的优势显而易见:
- 内存效率高:只存储变化部分,而非整个状态的快照。
- 速度快:应用增量通常比恢复整个快照更快。
- 支持细粒度操作:能精确控制每一次修改。
进阶玩法:给操作打上“标签”
你是否好奇用户“为什么”执行某个操作?比如,在图形编辑器中,你想在历史记录里展示“删除多边形”这样的描述。undoredo 支持在提交时附带元数据,完美满足这类需求:
use std::collections::HashMap;
use undoredo::{Delta, Recorder, UndoRedo};
// 定义操作命令类型
#[derive(Debug, Clone, PartialEq)]
enum Command {
InsertChar,
DeleteChar,
MoveChar,
}
fn main() {
let mut recorder: Recorder<HashMap<usize, char>> = Recorder::new(HashMap::new());
let mut undoredo: UndoRedo<Delta<HashMap<usize, char>>, Command> = UndoRedo::new();
recorder.insert(1, 'A');
recorder.insert(2, 'B');
// 提交时附带命令信息
undoredo.cmd_commit(Command::InsertChar, recorder.flush_delta());
// 查看历史记录
assert_eq!(undoredo.done().last().unwrap().cmd, Command::InsertChar);
undoredo.undo(&mut recorder);
// 撤销后,这个命令移到了“已撤销”栈中
assert_eq!(undoredo.undone().last().unwrap().cmd, Command::InsertChar);
}
在实现文本编辑器时,此特性非常有用,你可以记录每个操作是“插入字符”还是“删除字符”,并直接在界面上呈现给用户。
纯命令模式:当你不想要增量记录
有些场景下,你可能更偏爱纯粹的命令模式。没问题,undoredo 也支持这种用法:
use undoredo::UndoRedo;
// 定义命令
#[derive(Clone)]
struct AddCommand(i32);
fn main() {
let mut value = 0;
let mut undoredo: UndoRedo<(), AddCommand> = UndoRedo::new();
// 手动执行命令并记录
value += 5;
undoredo.cmd_commit(AddCommand(5), ());
value += 3;
undoredo.cmd_commit(AddCommand(3), ());
// 撤销:需要手动实现撤销逻辑
// 这里只是演示如何获取命令信息
if let Some(entry) = undoredo.done().last() {
println!("Last command: Add({})", entry.cmd.0);
}
}
这给了你极大的灵活性,不过命令的具体执行与撤销逻辑需要自行实现。
支持的数据结构:不只是 HashMap
undoredo 对标准库中的多种集合类型提供了开箱即用的支持:
HashMap - 哈希映射
HashSet - 哈希集合
BTreeMap - 有序映射
BTreeSet - 有序集合
Vec - 动态数组
此外,通过开启相应的 feature,它还能支持一些第三方库的数据结构,如 StableVec、thunderdome::Arena、rstar::RTree 等。这意味着你可以直接在这些类型上享受到完整的撤销重做能力。
自定义结构体:让你的类型也能“后悔”
如果你有自己的结构体,#[derive(Delta)] 宏能让你轻松为其赋予增量记录的能力:
use undoredo::Delta;
#[derive(Delta, Clone)]
struct Point {
x: f64,
y: f64,
}
#[derive(Delta, Clone)]
struct Polygon {
vertices: Vec<Point>,
color: String,
}
之后,你就能像操作标准库类型一样,对 Polygon 进行撤销和重做了。
实战案例:多边形集合编辑器
undoredo 仓库中包含了一个很酷的演示应用——多边形集合编辑器。用户可以在画布上动态添加和删除多边形,所有操作都支持撤销重做。该 Demo 利用 R 树(R-tree)空间索引来加速碰撞检测,undoredo 仅记录每次操作的增量变化,而非整个 R 树的快照。即便画布上有成千上万个多边形,撤销操作也能瞬间完成。
性能考量:增量 vs 快照
选择增量记录还是快照,取决于你的数据结构和使用场景:
- 增量记录适合:频繁的小修改、大数据结构的部分更新、需要细粒度撤销控制的场景。
- 快照适合:修改频率低、数据结构较小、需要快速跳转到任意历史状态的情景。
在多数场景下,undoredo 的增量记录方式都提供了出色的性能,其内存占用与修改次数成正比,而非数据结构本身的大小。
常见问题解答
Q: 支持并发操作吗?
A: 目前 undoredo 并非线程安全。若需在多线程环境使用,请自行引入锁机制。
Q: 可以设置最大历史记录数吗?
A: 库没有内置此限制,但你可以通过 UndoRedo 的方法手动管理历史记录栈。
Q: 支持序列化吗?
A: 是的,若你的数据类型实现了 Serialize / Deserialize,增量信息也可以被序列化。
Q: 和 git 的撤销有什么区别?
A: git 是版本控制系统,关注文件级别的历史变更。而 undoredo 是作用于内存中数据结构的撤销重做,关注应用运行时状态的改变。
总结
undoredo 是一个设计优雅的 Rust 库,它通过增量记录的方式,巧妙规避了传统命令模式的复杂性,为你提供了一套简单、高效且内存友好的撤销/重做方案。无论是开发图形编辑器、文本编辑器,还是任何需要“后悔药”的应用,它都是一个值得尝试的选择。
它的核心优势在于:
- 简单易用:寥寥数行代码即可集成。
- 内存高效:精准记录变化,拒绝冗余快照。
- 灵活强大:原生支持增量、快照和纯命令三种模式。
- 类型安全:借助 Rust 强大的类型系统,在编译期就杜绝许多潜在错误。
最后,引用编程界的一句名言:“人生没有撤销,但代码可以有。” 在开源项目中引入 undoredo,让你的用户也拥有“后悔”的权利吧!
推荐阅读