用Command模式简单的实现Undo&Redo功能

许多GUI程序中提供一个"撤销&重做"的功能,这个功能对用户来说非常友好;本文就简单的介绍一下如何用C#实现该功能。

实现Undo&Redo功能的基本模型是带撤销功能的命令模式,它将每步操作保存为一个命令对象,如下所示:

    interface Icommand
    {
        void Do();
        void Undo();
    }

其中Do函数执行功能,Undo函数回滚功能。这样就把命令给实体化了,只要将命令对象给保存下来,需要撤销时执行Undo函数,重做时执行Do函数即可。

有了这个基本思路后,下面就是实现细节了:

  1. 申请两个Stack来保存命令对象:UndoStack和RedoStack
  2. 执行命令时,将命令序列化为Command对象,执行Do方法,存入UndoStack,清空RedoStack
  3. 撤销命令时,从UndoStack中取出命令,执行Undo方法,存入RedoStack
  4. 重做命令时,从RedoStack中取出命令,执行Do方法,存入UndoStack

一个简单的实现如下:

    class CommandManager
    {
        Stack<Command> redoStack = new Stack<Command>();
        Stack<Command> undoStack = new Stack<Command>();

        public void AddCommand(Action doCmd, Action undoCmd)
        {
            var cmd = new Command(doCmd, undoCmd);
            cmd.Do();

            undoStack.Push(cmd);
            redoStack.Clear();
        }

        public bool Undo()
        {
            if (undoStack.Count == 0)
                return false;

            var cmd = undoStack.Pop();
            redoStack.Push(cmd);

            cmd.Undo();
            return true;
        }

        public bool Redo()
        {
            if (redoStack.Count == 0)
                return false;

            var cmd = redoStack.Pop();
            undoStack.Push(cmd);

            cmd.Do();
            return true;
        }

        class Command
        {
            public Action Do { get; private set; }
            public Action Undo { get; private set; }

            public Command(Action doCmd, Action undoCmd)
            {
                this.Do = doCmd;
                this.Undo = undoCmd;
            }
        }
    }

用C#实现起来还是非常简洁的,就几十行代码。

遗留问题:命令对象何时释放

前面的实现虽然非常简单,但存在一个遗留问题:每一个命令对象都保存在UndoStack中了,这样随着程序的执行,UndoStack中记录的命令越来越多,占用内存得不到释放。对于这个问题,一般有如下几种策略:

  1. 不释放命令对象。一般需要Undo&Redo功能都是些GUI程序,这些程序大多不会持续运行,并且对内存的占用也没有太大限制,命令对象一般也不会占用多少内存。保存所有命令对象不会对程序造成什么影响。
  2. 命令堆栈维持固定的长度:当命令堆栈的长度超过阈值的时候,删除最开始压入的命令。这种策略用得最多,但这样带来的问题就是无法实现无限Undo。
  3. 将命令堆栈保存到文件:将命令序列化保存到文件,需要使用时从文件中还原。这种方式可以实现无限Undo,但序列化命令往往是件比较麻烦的事情,反序列化时也要消耗时间。
  4. 综合2,3两种方案:内存中保持固定长度的命令对象,超过阈值的保存到文件。这种方式能有效解决反序列化的耗时问题,也能实现无限Undo。但实现起来也最为麻烦。

基于篇幅所限,本文就不进一步讨论和实现了。

原文地址:https://www.cnblogs.com/TianFang/p/3086820.html