找回密码
立即注册
搜索
热搜: Java Python Linux Go
发回帖 发新帖

5274

积分

0

好友

717

主题
发表于 4 小时前 | 查看: 6| 回复: 0

你是否曾在编码或编辑文档时,手滑误删了关键内容,然后疯狂按下 Ctrl+Z 却发现无法挽回?在软件开发中,为用户提供撤销/重做(Undo/Redo)功能就像给应用装上了一台“时光机”。今天要介绍的 undoredo 库,正是为 Rust 生态量身定做的这样一款利器。

为什么我们需要撤销/重做?

设想你正在构建一个图形编辑器。用户画了一个多边形,再画一个,然后想删除第一个。如果没有撤销功能,误操作将带来极差的体验。软件开发中,实现撤销/重做主要有几种经典设计模式

  1. 命令模式(Command Pattern):将每个操作封装为命令对象。
  2. 备忘录模式(Memento Pattern):保存状态的完整快照。
  3. 增量记录(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 则管理历史状态。你只需调用 commitundoredo 这几个方法,便实现了完整的撤销重做功能。

原理揭秘:增量记录是如何工作的?

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,它还能支持一些第三方库的数据结构,如 StableVecthunderdome::Arenarstar::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 库,它通过增量记录的方式,巧妙规避了传统命令模式的复杂性,为你提供了一套简单、高效且内存友好的撤销/重做方案。无论是开发图形编辑器、文本编辑器,还是任何需要“后悔药”的应用,它都是一个值得尝试的选择。

它的核心优势在于:

  1. 简单易用:寥寥数行代码即可集成。
  2. 内存高效:精准记录变化,拒绝冗余快照。
  3. 灵活强大:原生支持增量、快照和纯命令三种模式。
  4. 类型安全:借助 Rust 强大的类型系统,在编译期就杜绝许多潜在错误。

最后,引用编程界的一句名言:“人生没有撤销,但代码可以有。” 在开源项目中引入 undoredo,让你的用户也拥有“后悔”的权利吧!

推荐阅读




上一篇:分布式系统必懂:CAP、BASE、SOLID、KISS 缩写到底指什么?
下一篇:2026年ASIC增速将超GPU?NVIDIA纵向扩展与互联技术深度解析
您需要登录后才可以回帖 登录 | 立即注册

手机版|小黑屋|网站地图|云栈社区 ( 苏ICP备2022046150号-2 )

GMT+8, 2026-5-7 10:32 , Processed in 0.762115 second(s), 41 queries , Gzip On.

Powered by Discuz! X3.5

© 2025-2026 云栈社区.

快速回复 返回顶部 返回列表