Как хранить Действия, которые не препятствуют сборке мусора переменных, которые они используют в области видимости - PullRequest
3 голосов
/ 14 сентября 2011

Я пытаюсь исправить проблему сбора мусора в приложении 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 сохраняют ссылки на них.Реальный код группирует различные модели представления в сгруппированные недолисты (модели представления, которые содержат дочерние модели представления, используют один и тот же стек отмены), поэтому трудно понять, когда и где поместить очистку.

Мне было интересно, существует ли какой-либо методчтобы эти действия истекли, если они единственные, хранящие ссылки на переменные внутри них?

Предложения приветствуются!

Ответы [ 2 ]

4 голосов
/ 14 сентября 2011

У меня есть решение для вас.Мне не нравится использование 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), который не былt строго напечатан и может привести к ошибкам во время выполнения.

    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);

Пожалуйста, дайте мне знать, если смогупредоставить более подробную информацию о чем-либо.

0 голосов
/ 14 сентября 2011

Если вы не можете очистить его любым другим способом, вы можете использовать WeakReference для хранения свойства, но я думаю, что будут другие проблемы, потому что это все равно приведет к существованию экземпляра Action с нулевой ссылкой, прикрепленной к нему.

В качестве краткого обзора я был бы более склонен использовать синглтон для хранения регистрации в модели и позволить модели управлять списком экземпляров всех прикрепленных к ней действий по отмене.Когда модель выходит из области видимости, вызовите для нее метод очистки или реализуйте интерфейс типа IDisposable, если это подходит.Однако, в зависимости от реализации, вам может не понадобиться синглтон.

...