如何存储不阻止它们在作用域中使用的变量的垃圾收集的操作
本文关键字:变量 操作 作用域 存储 何存储 | 更新日期: 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类型接口(如果合适的话)。但是,根据实现的不同,您可能根本不需要单例。