如何存储不阻止在范围内使用的变量的垃圾收集的操作

时间:2011-09-13 23:40:18

标签: c# .net garbage-collection reference

我正在尝试修复使用以下撤消堆栈模型的MVVM应用程序的垃圾收集问题。

该示例非常简约,真实世界代码有很大不同,每个ViewModel使用工厂类撤消列表而不是单个undolist但是具有代表性:

using System;
using System.Collections.Generic;
using System.Text;
using System.Diagnostics;
using System.Reflection;
using System.ComponentModel;
using System.Linq;

namespace ConsoleApplication9
{
    public class UndoList
    {
        public bool IsUndoing { get; set; }

        private Stack<Action> _undo = new Stack<Action>();

        public Stack<Action> Undo
        {
            get { return _undo; }
            set { _undo = value; }
        }

        private static UndoList _instance;
        // singleton of the undo stack
        public static UndoList Instance
        {
            get
            {
                if (_instance == null)
                {
                    _instance = new UndoList();
                }
                return _instance;
            }
        }
    }

    public class ViewModel : INotifyPropertyChanged
    {
        public event PropertyChangedEventHandler PropertyChanged;

        // execute the last undo operation
        public void Undo()
        {
            UndoList.Instance.IsUndoing = true;
            var action = UndoList.Instance.Undo.Pop();
            action();
            UndoList.Instance.IsUndoing = false;
        }

        // push an action into the undo stack
        public void AddUndo(Action action)
        {
            if (UndoList.Instance.IsUndoing) return;

            UndoList.Instance.Undo.Push(action);
        }

        // create push an action into the undo stack that resets a property value
        public void AddUndo(string propertyName, object oldValue)
        {
            if (UndoList.Instance.IsUndoing) return;

            var property = this.GetType().GetProperties().First(p => p.Name == propertyName);

            Action action = () =>
            {
                property.SetValue(this, oldValue, null);
            };

            UndoList.Instance.Undo.Push(action);
        }
    }

    public class TestModel : ViewModel
    {
        private bool _testProperty;
        public bool TestProperty
        {
            get
            {
                return _testProperty;
            }
            set
            {
                base.AddUndo("TestProperty", _testProperty);
                _testProperty = value;
            }
        }

        // mock property indicating if a business action has been done for test
        private bool _hasBusinessActionBeenDone;
        public bool HasBusinessActionBeenDone
        {
            get
            {
                return _hasBusinessActionBeenDone;
            }
            set
            {
                _hasBusinessActionBeenDone = value;
            }
        }

        public void DoBusinessAction()
        {
            AddUndo(() => { inverseBusinessAction(); });
            businessAction();
        }

        private void businessAction()
        {
            // using fake property for brevity of example
            this.HasBusinessActionBeenDone = true;
        }

        private void inverseBusinessAction()
        {
            // using fake property for brevity of example
            this.HasBusinessActionBeenDone = false;
        }

    }

    class Program
    {
        static void Test()
        {
            var vm = new TestModel();

            // test undo of property
            vm.TestProperty = true;
            vm.Undo();
            Debug.Assert(vm.TestProperty == false);

            // test undo of business action
            vm.DoBusinessAction();
            vm.Undo();
            Debug.Assert(vm.HasBusinessActionBeenDone == false);

            // do it once more without Undo, so the undo stack has something
            vm.DoBusinessAction();
        }

        static void Main(string[] args)
        {
            Program.Test();
            GC.Collect(GC.MaxGeneration, GCCollectionMode.Forced);

            // at this point UndoList.Instance.Undo
            // contains an Action which references the TestModel
            // which will never be collected...
            // in real world code knowing when to clear this is a problem
            // because it is a singleton factory class for undolists per viewmodel type
            // ideally would be to clear the list when there are no more references
            // to the viewmodel type in question, but the Actions in the list prevent that
        }
    }
}

您会看到当任何viewModel超出范围时,UndoList中的操作会保留对它们的引用。真实代码将各种视图模型分组为分组的undolists(包含子视图模型的viewModel共享相同的撤消堆栈),因此很难知道清除的时间和位置。

我想知道是否有一些方法可以使这些操作过期,如果他们是唯一一个保持对其中变量的引用的方法?

建议欢迎!

2 个答案:

答案 0 :(得分:4)

我有一个解决方案给你。我不喜欢使用UndoList作为单身人士,但我保留了它以便为您提供问题的直接答案。在实践中,我不会使用单身人士。

现在,您将发现很难避免在操作中捕获对视图模型的引用。如果你尝试过,它会让你的代码变得非常丑陋。最好的方法是让您的视图模型实现IDisposable,并确保在超出范围时将其丢弃。请记住,垃圾收集器从不调用Dispose,因此您必须这样做。

  

使用IDisposable是清理时的标准模型   不再需要实例。

因此,首先要定义的是一个帮助器类,它在处理时执行一个动作。

public sealed class AnonymousDisposable : IDisposable
{
    private readonly Action _dispose;
    private int _isDisposed;

    public AnonymousDisposable(Action dispose)
    {
        _dispose = dispose;
    }

    public void Dispose()
    {
        if (Interlocked.Exchange(ref _isDisposed, 1) == 0)
        {
            _dispose();
        }
    }
}

现在我可以编写这样的内容来删除列表中的元素:

var disposable = new AnonymousDisposable(() => list.Remove(item));

稍后,当我致电disposable.Dispose()时,该项目将从列表中删除。

现在,这是您的代码重新实现。

我已将UndoList更改为静态类,而不是单例。如果需要,您可以将其更改回来。

public static class UndoList
{
    public static bool IsUndoing { get; private set; }
    private static List<Action> _undos = new List<Action>();

    public static IDisposable AddUndo(Action action)
    {
        var disposable = (IDisposable)null;
        if (!IsUndoing)
        {           
            disposable = new AnonymousDisposable(() => _undos.Remove(action));
            _undos.Add(action);
        }
        return disposable ?? new AnonymousDisposable(() => { });
    }

    public static bool Undo()
    {
        IsUndoing = true;
        var result = _undos.Count > 0;
        if (result)
        {
            var action = _undos[_undos.Count - 1];
            _undos.Remove(action);
            action();
        }
        IsUndoing = false;
        return result;
    }
}

你会注意到我用一个列表替换了堆栈。我这样做是因为我需要从列表中删除项目。

此外,您可以看到AddUndo现在返回IDisposable。调用代码需要保持返回的一次性,并在想要从列表中删除操作时调用Dispose

我还内化了Undo行动。在视图模型中使用它没有意义。调用Undo会有效地弹出列表中的顶部项目并执行操作并返回true。但是,如果列表为空,则返回false。您可以将其用于测试目的。

ViewModel类现在看起来像这样:

public class ViewModel : IDisposable, INotifyPropertyChanged
{
    public ViewModel()
    {
        _disposables = new List<IDisposable>();
        _disposable = new AnonymousDisposable(() =>
            _disposables.ForEach(d => d.Dispose()));
    }

    private readonly List<IDisposable> _disposables;
    private readonly IDisposable _disposable;

    public void Dispose()
    {
        _disposable.Dispose();
    }

    public event PropertyChangedEventHandler PropertyChanged;

    protected void AddUndo(Action action)
    { ... }

    protected void SetUndoableValue<T>(Action<T> action, T newValue, T oldValue)
    { ... }
}

它实现IDisposable并在内部跟踪一次性用品列表和匿名一次性用品,当视图模型本身被丢弃时,它将处理列表中的项目。呼!满口,但我希望这是有道理的。

AddUndo方法正文如下:

    protected void AddUndo(Action action)
    {
        var disposable = (IDisposable)null;
        Action inner = () =>
        {
            _disposables.Remove(disposable);
            action();
        };
        disposable = UndoList.AddUndo(inner);
        _disposables.Add(disposable);
    }

在内部调用UndoList.AddUndo传递一个操作,该操作将在调用IDisposable时从视图模型的撤消操作列表中删除返回的UndoList.Undo() - 以及重要的是,实际执行行动。

因此,这意味着在处理视图模型时,将从撤消列表中删除所有未完成的撤消操作,并且在调用Undo时,将从视图模型中删除关联的一次性。 这可确保您在处理视图模型时不会保留对视图模型的引用。

我创建了一个名为SetUndoableValue的辅助函数,它取代了您的void AddUndo(string propertyName, object oldValue)方法,该方法不是强类型的,可能会导致运行时错误。

    protected void SetUndoableValue<T>(Action<T> action, T newValue, T oldValue)
    {
        this.AddUndo(() => action(oldValue));
        action(newValue);
    }

我将这两种方法protected视为public似乎过于混乱。

TestModel或多或少相同:

public class TestModel : ViewModel
{
    private bool _testProperty;
    public bool TestProperty
    {
        get { return _testProperty; }
        set
        {
            this.SetUndoableValue(v => _testProperty = v, value, _testProperty);
        }
    }

    public bool HasBusinessActionBeenDone { get; set; }

    public void DoBusinessAction()
    {
        this.AddUndo(this.inverseBusinessAction);
        businessAction();
    }

    private void businessAction()
    {
        this.HasBusinessActionBeenDone = true;
    }

    private void inverseBusinessAction()
    {
        this.HasBusinessActionBeenDone = false;
    }
}

最后,这是正确测试UndoList函数的代码:

using (var vm = new TestModel())
{
    Debug.Assert(UndoList.Undo() == false);

    vm.TestProperty = true;

    Debug.Assert(UndoList.Undo() == true);
    Debug.Assert(UndoList.Undo() == false);

    Debug.Assert(vm.TestProperty == false);

    vm.DoBusinessAction();

    Debug.Assert(UndoList.Undo() == true);

    Debug.Assert(vm.HasBusinessActionBeenDone == false);

    vm.DoBusinessAction();
}

Debug.Assert(UndoList.Undo() == false);

如果我能提供任何更详细的信息,请告诉我。

答案 1 :(得分:0)

如果你不能以任何其他方式清理它,你可以使用WeakReference来保存属性,但我认为会有其他问题,因为这仍然会导致一个Action实例存在并附加一个空引用。

快速浏览一下,我更倾向于使用单例来保持对模型的注册,并让模型管理附加到其上的所有撤消操作的实例列表。当模型超出范围时,调用它上面的清理方法或在其上实现IDisposable类型接口,如果这适合的话。但是,根据实施情况,您可能不需要单身人士。