如何存储不阻止它们在作用域中使用的变量的垃圾收集的操作

本文关键字:变量 操作 作用域 存储 何存储 | 更新日期: 2023-09-27 18:08:44

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

这个例子是非常简约的,现实世界的代码是非常不同的,每个ViewModel使用一个撤消列表的工厂类,而不是一个撤消列表,但具有代表性:

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,但我保留了它,以便为您的问题提供直接的答案。在实践中,我不会使用单例。

现在,您将发现很难避免在操作中捕获对视图模型的引用。如果您尝试这样做,将使您的代码非常难看。最好的方法是让视图模型实现IDisposable,并确保在它们超出作用域时处理它们。请记住,垃圾收集器永远不会调用Dispose,所以您必须。

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

所以首先要定义的是一个helper类,它在被处置时执行一个操作。

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传递一个动作,当UndoList.Undo()被调用时,将从视图模型的撤消动作列表中删除返回的IDisposable,以及,重要的是,实际执行该动作。

因此,这意味着当视图模型被处置时,所有未完成的撤消操作都从撤消列表中删除,并且当调用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);

如果我能提供更多的细节,请告诉我。

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

作为快速查看,我更倾向于使用单例来保持对模型的注册,并让模型管理附加到它的所有撤消操作的实例列表。当模型超出作用域时,在其上调用清理方法或在其上实现一个IDisposable类型接口(如果合适的话)。但是,根据实现的不同,您可能根本不需要单例。